Skip to content

Commit 0547f4e

Browse files
authored
API for managing RTDB Security Rules (#595)
* Adding RTDB rules management APIs * Added more test cases * Getting to 100% unit test coverage * Added more input validation; tests * Handling URLs with query params * Rejecting on invalid arguments * Removing unused attribute * Removing unused imports * Cleaned up the tests * Updated changelog * Updated documentation text
1 parent e963005 commit 0547f4e

File tree

6 files changed

+509
-13
lines changed

6 files changed

+509
-13
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Unreleased
22

3-
-
3+
- [added] `admin.database().getRules()` method to retrieve the currently
4+
applied RTDB rules text.
5+
- [added] `admin.database().getRulesJSON()` method to retrieve the currently
6+
applied RTDB rules as a parsed JSON object.
7+
- [added] `admin.database().setRules()` method to update the RTDB rules.
48

59
# v8.2.0
610

src/database/database.ts

Lines changed: 146 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import {URL} from 'url';
2+
import * as path from 'path';
3+
14
import {FirebaseApp} from '../firebase-app';
2-
import {FirebaseDatabaseError} from '../utils/error';
5+
import {FirebaseDatabaseError, AppErrorCodes} from '../utils/error';
36
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
47
import {Database} from '@firebase/database';
58

69
import * as validator from '../utils/validator';
10+
import { AuthorizedHttpClient, HttpRequestConfig, HttpError } from '../utils/api-request';
711

812
/**
913
* This variable is redefined in the firebase-js-sdk. Before modifying this
@@ -37,11 +41,19 @@ class DatabaseInternals implements FirebaseServiceInternalsInterface {
3741
}
3842
}
3943

44+
declare module '@firebase/database' {
45+
interface Database {
46+
getRules(): Promise<string>;
47+
getRulesJSON(): Promise<object>;
48+
setRules(source: string | Buffer | object): Promise<void>;
49+
}
50+
}
51+
4052
export class DatabaseService implements FirebaseServiceInterface {
4153

42-
public INTERNAL: DatabaseInternals = new DatabaseInternals();
54+
public readonly INTERNAL: DatabaseInternals = new DatabaseInternals();
4355

44-
private appInternal: FirebaseApp;
56+
private readonly appInternal: FirebaseApp;
4557

4658
constructor(app: FirebaseApp) {
4759
if (!validator.isNonNullObject(app) || !('options' in app)) {
@@ -76,6 +88,18 @@ export class DatabaseService implements FirebaseServiceInterface {
7688
const rtdb = require('@firebase/database');
7789
const { version } = require('../../package.json');
7890
db = rtdb.initStandalone(this.appInternal, dbUrl, version).instance;
91+
92+
const rulesClient = new DatabaseRulesClient(this.app, dbUrl);
93+
db.getRules = () => {
94+
return rulesClient.getRules();
95+
};
96+
db.getRulesJSON = () => {
97+
return rulesClient.getRulesJSON();
98+
};
99+
db.setRules = (source) => {
100+
return rulesClient.setRules(source);
101+
};
102+
79103
this.INTERNAL.databases[dbUrl] = db;
80104
}
81105
return db;
@@ -97,3 +121,122 @@ export class DatabaseService implements FirebaseServiceInterface {
97121
});
98122
}
99123
}
124+
125+
const RULES_URL_PATH = '.settings/rules.json';
126+
127+
/**
128+
* A helper client for managing RTDB security rules.
129+
*/
130+
class DatabaseRulesClient {
131+
132+
private readonly dbUrl: string;
133+
private readonly httpClient: AuthorizedHttpClient;
134+
135+
constructor(app: FirebaseApp, dbUrl: string) {
136+
const parsedUrl = new URL(dbUrl);
137+
parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH);
138+
this.dbUrl = parsedUrl.toString();
139+
this.httpClient = new AuthorizedHttpClient(app);
140+
}
141+
142+
/**
143+
* Gets the currently applied security rules as a string. The return value consists of
144+
* the rules source including comments.
145+
*
146+
* @return {Promise<string>} A promise fulfilled with the rules as a raw string.
147+
*/
148+
public getRules(): Promise<string> {
149+
const req: HttpRequestConfig = {
150+
method: 'GET',
151+
url: this.dbUrl,
152+
};
153+
return this.httpClient.send(req)
154+
.then((resp) => {
155+
return resp.text;
156+
})
157+
.catch((err) => {
158+
throw this.handleError(err);
159+
});
160+
}
161+
162+
/**
163+
* Gets the currently applied security rules as a parsed JSON object. Any comments in
164+
* the original source are stripped away.
165+
*
166+
* @return {Promise<object>} A promise fulfilled with the parsed rules source.
167+
*/
168+
public getRulesJSON(): Promise<object> {
169+
const req: HttpRequestConfig = {
170+
method: 'GET',
171+
url: this.dbUrl,
172+
data: {format: 'strict'},
173+
};
174+
return this.httpClient.send(req)
175+
.then((resp) => {
176+
return resp.data;
177+
})
178+
.catch((err) => {
179+
throw this.handleError(err);
180+
});
181+
}
182+
183+
/**
184+
* Sets the specified rules on the Firebase Database instance. If the rules source is
185+
* specified as a string or a Buffer, it may include comments.
186+
*
187+
* @param {string|Buffer|object} source Source of the rules to apply. Must not be `null`
188+
* or empty.
189+
* @return {Promise<void>} Resolves when the rules are set on the Database.
190+
*/
191+
public setRules(source: string | Buffer | object): Promise<void> {
192+
if (!validator.isNonEmptyString(source) &&
193+
!validator.isBuffer(source) &&
194+
!validator.isNonNullObject(source)) {
195+
const error = new FirebaseDatabaseError({
196+
code: 'invalid-argument',
197+
message: 'Source must be a non-empty string, Buffer or an object.',
198+
});
199+
return Promise.reject(error);
200+
}
201+
202+
const req: HttpRequestConfig = {
203+
method: 'PUT',
204+
url: this.dbUrl,
205+
data: source,
206+
headers: {
207+
'content-type': 'application/json; charset=utf-8',
208+
},
209+
};
210+
return this.httpClient.send(req)
211+
.then(() => {
212+
return;
213+
})
214+
.catch((err) => {
215+
throw this.handleError(err);
216+
});
217+
}
218+
219+
private handleError(err: Error): Error {
220+
if (err instanceof HttpError) {
221+
return new FirebaseDatabaseError({
222+
code: AppErrorCodes.INTERNAL_ERROR,
223+
message: this.getErrorMessage(err),
224+
});
225+
}
226+
return err;
227+
}
228+
229+
private getErrorMessage(err: HttpError): string {
230+
const intro = 'Error while accessing security rules';
231+
try {
232+
const body: {error?: string} = err.response.data;
233+
if (body && body.error) {
234+
return `${intro}: ${body.error.trim()}`;
235+
}
236+
} catch {
237+
// Ignore parsing errors
238+
}
239+
240+
return `${intro}: ${err.response.text}`;
241+
}
242+
}

src/index.d.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2064,6 +2064,31 @@ declare namespace admin.database {
20642064
* @return A `Reference` pointing to the provided Firebase URL.
20652065
*/
20662066
refFromURL(url: string): admin.database.Reference;
2067+
2068+
/**
2069+
* Gets the currently applied security rules as a string. The return value consists of
2070+
* the rules source including comments.
2071+
*
2072+
* @return A promise fulfilled with the rules as a raw string.
2073+
*/
2074+
getRules(): Promise<string>;
2075+
2076+
/**
2077+
* Gets the currently applied security rules as a parsed JSON object. Any comments in
2078+
* the original source are stripped away.
2079+
*
2080+
* @return A promise fulfilled with the parsed rules object.
2081+
*/
2082+
getRulesJSON(): Promise<object>;
2083+
2084+
/**
2085+
* Sets the specified rules on the Firebase Realtime Database instance. If the rules source is
2086+
* specified as a string or a Buffer, it may include comments.
2087+
*
2088+
* @param source Source of the rules to apply. Must not be `null` or empty.
2089+
* @return Resolves when the rules are set on the Realtime Database.
2090+
*/
2091+
setRules(source: string | Buffer | object): Promise<void>;
20672092
}
20682093

20692094
/**

test/integration/database.spec.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import url = require('url');
2121
import {defaultApp, nullApp, nonNullApp, cmdArgs, databaseUrl} from './setup';
2222

2323
/* tslint:disable:no-var-requires */
24-
const apiRequest = require('../../lib/utils/api-request');
2524
const chalk = require('chalk');
2625
/* tslint:enable:no-var-requires */
2726

@@ -43,20 +42,13 @@ describe('admin.database', () => {
4342
}
4443
console.log(chalk.yellow(' Updating security rules to defaults.'));
4544
/* tslint:enable:no-console */
46-
const client = new apiRequest.AuthorizedHttpClient(defaultApp);
47-
const dbUrl = url.parse(databaseUrl);
4845
const defaultRules = {
4946
rules : {
5047
'.read': 'auth != null',
5148
'.write': 'auth != null',
5249
},
5350
};
54-
return client.send({
55-
url: `https://${dbUrl.host}/.settings/rules.json`,
56-
method: 'PUT',
57-
data: defaultRules,
58-
timeout: 10000,
59-
});
51+
return admin.database().setRules(defaultRules);
6052
});
6153

6254
it('admin.database() returns a database client', () => {
@@ -166,6 +158,18 @@ describe('admin.database', () => {
166158
return refWithUrl.remove().should.eventually.be.fulfilled;
167159
});
168160
});
161+
162+
it('admin.database().getRules() returns currently defined rules as a string', () => {
163+
return admin.database().getRules().then((result) => {
164+
return expect(result).to.be.not.empty;
165+
});
166+
});
167+
168+
it('admin.database().getRulesJSON() returns currently defined rules as an object', () => {
169+
return admin.database().getRulesJSON().then((result) => {
170+
return expect(result).to.be.not.undefined;
171+
});
172+
});
169173
});
170174

171175
function addValueEventListener(

0 commit comments

Comments
 (0)