Skip to content

Commit 471aa49

Browse files
authored
Implemented createRulesFileFromSource() and createRuleset() APIs (#607)
* Implementing the Firebase Security Rules API * More argument validation and assertions * Adding the rest of the CRUD operations for rulesets * Cleaning up the rules impl * Cleaned up tests * Adding some missing comments * Removing support for multiple rules files in create()
1 parent e5f0ed3 commit 471aa49

4 files changed

Lines changed: 478 additions & 81 deletions

File tree

src/security-rules/security-rules-api-client.ts

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,25 @@ import { PrefixedFirebaseError } from '../utils/error';
1919
import { FirebaseSecurityRulesError, SecurityRulesErrorCode } from './security-rules-utils';
2020
import * as validator from '../utils/validator';
2121

22-
const RULES_API_URL = 'https://firebaserules.googleapis.com/v1';
22+
const RULES_V1_API = 'https://firebaserules.googleapis.com/v1';
23+
24+
export interface Release {
25+
readonly name: string;
26+
readonly rulesetName: string;
27+
readonly createTime: string;
28+
readonly updateTime: string;
29+
}
30+
31+
export interface RulesetContent {
32+
readonly source: {
33+
readonly files: Array<{name: string, content: string}>;
34+
};
35+
}
36+
37+
export interface RulesetResponse extends RulesetContent {
38+
readonly name: string;
39+
readonly createTime: string;
40+
}
2341

2442
/**
2543
* Class that facilitates sending requests to the Firebase security rules backend API.
@@ -31,6 +49,11 @@ export class SecurityRulesApiClient {
3149
private readonly url: string;
3250

3351
constructor(private readonly httpClient: HttpClient, projectId: string) {
52+
if (!validator.isNonNullObject(httpClient)) {
53+
throw new FirebaseSecurityRulesError(
54+
'invalid-argument', 'HttpClient must be a non-null object.');
55+
}
56+
3457
if (!validator.isNonEmptyString(projectId)) {
3558
throw new FirebaseSecurityRulesError(
3659
'invalid-argument',
@@ -39,7 +62,49 @@ export class SecurityRulesApiClient {
3962
+ 'environment variable.');
4063
}
4164

42-
this.url = `${RULES_API_URL}/projects/${projectId}`;
65+
this.url = `${RULES_V1_API}/projects/${projectId}`;
66+
}
67+
68+
public getRuleset(name: string): Promise<RulesetResponse> {
69+
return Promise.resolve()
70+
.then(() => {
71+
return this.getRulesetName(name);
72+
})
73+
.then((rulesetName) => {
74+
return this.getResource<RulesetResponse>(rulesetName);
75+
});
76+
}
77+
78+
public createRuleset(ruleset: RulesetContent): Promise<RulesetResponse> {
79+
if (!validator.isNonNullObject(ruleset) ||
80+
!validator.isNonNullObject(ruleset.source) ||
81+
!validator.isNonEmptyArray(ruleset.source.files)) {
82+
83+
const err = new FirebaseSecurityRulesError('invalid-argument', 'Invalid rules content.');
84+
return Promise.reject(err);
85+
}
86+
87+
for (const rf of ruleset.source.files) {
88+
if (!validator.isNonNullObject(rf) ||
89+
!validator.isNonEmptyString(rf.name) ||
90+
!validator.isNonEmptyString(rf.content)) {
91+
92+
const err = new FirebaseSecurityRulesError(
93+
'invalid-argument', `Invalid rules file argument: ${JSON.stringify(rf)}`);
94+
return Promise.reject(err);
95+
}
96+
}
97+
98+
const request: HttpRequestConfig = {
99+
method: 'POST',
100+
url: `${this.url}/rulesets`,
101+
data: ruleset,
102+
};
103+
return this.sendRequest<RulesetResponse>(request);
104+
}
105+
106+
public getRelease(name: string): Promise<Release> {
107+
return this.getResource<Release>(`releases/${name}`);
43108
}
44109

45110
/**
@@ -49,11 +114,29 @@ export class SecurityRulesApiClient {
49114
* @param {string} name Full qualified name of the resource to get.
50115
* @returns {Promise<T>} A promise that fulfills with the resource.
51116
*/
52-
public getResource<T>(name: string): Promise<T> {
117+
private getResource<T>(name: string): Promise<T> {
53118
const request: HttpRequestConfig = {
54119
method: 'GET',
55120
url: `${this.url}/${name}`,
56121
};
122+
return this.sendRequest<T>(request);
123+
}
124+
125+
private getRulesetName(name: string): string {
126+
if (!validator.isNonEmptyString(name)) {
127+
throw new FirebaseSecurityRulesError(
128+
'invalid-argument', 'Ruleset name must be a non-empty string.');
129+
}
130+
131+
if (name.indexOf('/') !== -1) {
132+
throw new FirebaseSecurityRulesError(
133+
'invalid-argument', 'Ruleset name must not contain any "/" characters.');
134+
}
135+
136+
return `rulesets/${name}`;
137+
}
138+
139+
private sendRequest<T>(request: HttpRequestConfig): Promise<T> {
57140
return this.httpClient.send(request)
58141
.then((resp) => {
59142
return resp.data as T;

src/security-rules/security-rules.ts

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../
1818
import { FirebaseApp } from '../firebase-app';
1919
import * as utils from '../utils/index';
2020
import * as validator from '../utils/validator';
21-
import { SecurityRulesApiClient } from './security-rules-api-client';
21+
import { SecurityRulesApiClient, RulesetResponse, RulesetContent } from './security-rules-api-client';
2222
import { AuthorizedHttpClient } from '../utils/api-request';
2323
import { FirebaseSecurityRulesError } from './security-rules-utils';
2424

@@ -38,21 +38,6 @@ export interface RulesetMetadata {
3838
readonly createTime: string;
3939
}
4040

41-
interface Release {
42-
readonly name: string;
43-
readonly rulesetName: string;
44-
readonly createTime: string;
45-
readonly updateTime: string;
46-
}
47-
48-
interface RulesetResponse {
49-
readonly name: string;
50-
readonly createTime: string;
51-
readonly source: {
52-
readonly files: RulesFile[];
53-
};
54-
}
55-
5641
/**
5742
* Represents a set of Firebase security rules.
5843
*/
@@ -114,20 +99,7 @@ export class SecurityRules implements FirebaseServiceInterface {
11499
* @returns {Promise<Ruleset>} A promise that fulfills with the specified Ruleset.
115100
*/
116101
public getRuleset(name: string): Promise<Ruleset> {
117-
if (!validator.isNonEmptyString(name)) {
118-
const err = new FirebaseSecurityRulesError(
119-
'invalid-argument', 'Ruleset name must be a non-empty string.');
120-
return Promise.reject(err);
121-
}
122-
123-
if (name.indexOf('/') !== -1) {
124-
const err = new FirebaseSecurityRulesError(
125-
'invalid-argument', 'Ruleset name must not contain any "/" characters.');
126-
return Promise.reject(err);
127-
}
128-
129-
const resource = `rulesets/${name}`;
130-
return this.client.getResource<RulesetResponse>(resource)
102+
return this.client.getRuleset(name)
131103
.then((rulesetResponse) => {
132104
return new Ruleset(rulesetResponse);
133105
});
@@ -143,9 +115,57 @@ export class SecurityRules implements FirebaseServiceInterface {
143115
return this.getRulesetForRelease(SecurityRules.CLOUD_FIRESTORE);
144116
}
145117

118+
/**
119+
* Creates a `RulesFile` with the given name and source. Throws if any of the arguments are invalid. This is a
120+
* local operation, and does not involve any network API calls.
121+
*
122+
* @param {string} name Name to assign to the rules file.
123+
* @param {string|Buffer} source Contents of the rules file.
124+
* @returns {RulesFile} A new rules file instance.
125+
*/
126+
public createRulesFileFromSource(name: string, source: string | Buffer): RulesFile {
127+
if (!validator.isNonEmptyString(name)) {
128+
throw new FirebaseSecurityRulesError(
129+
'invalid-argument', 'Name must be a non-empty string.');
130+
}
131+
132+
let content: string;
133+
if (validator.isNonEmptyString(source)) {
134+
content = source;
135+
} else if (validator.isBuffer(source)) {
136+
content = source.toString('utf-8');
137+
} else {
138+
throw new FirebaseSecurityRulesError(
139+
'invalid-argument', 'Source must be a non-empty string or a Buffer.');
140+
}
141+
142+
return {
143+
name,
144+
content,
145+
};
146+
}
147+
148+
/**
149+
* Creates a new `Ruleset` from the given `RulesFile`.
150+
*
151+
* @param {RulesFile} file Rules file to include in the new Ruleset.
152+
* @returns {Promise<Ruleset>} A promise that fulfills with the newly created Ruleset.
153+
*/
154+
public createRuleset(file: RulesFile): Promise<Ruleset> {
155+
const ruleset: RulesetContent = {
156+
source: {
157+
files: [ file ],
158+
},
159+
};
160+
161+
return this.client.createRuleset(ruleset)
162+
.then((rulesetResponse) => {
163+
return new Ruleset(rulesetResponse);
164+
});
165+
}
166+
146167
private getRulesetForRelease(releaseName: string): Promise<Ruleset> {
147-
const resource = `releases/${releaseName}`;
148-
return this.client.getResource<Release>(resource)
168+
return this.client.getRelease(releaseName)
149169
.then((release) => {
150170
const rulesetName = release.rulesetName;
151171
if (!validator.isNonEmptyString(rulesetName)) {

0 commit comments

Comments
 (0)