Skip to content

Commit

Permalink
Implemented createRulesFileFromSource() and createRuleset() APIs (#607)
Browse files Browse the repository at this point in the history
* 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()
  • Loading branch information
hiranya911 authored Aug 1, 2019
1 parent e5f0ed3 commit 471aa49
Show file tree
Hide file tree
Showing 4 changed files with 478 additions and 81 deletions.
89 changes: 86 additions & 3 deletions src/security-rules/security-rules-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,25 @@ import { PrefixedFirebaseError } from '../utils/error';
import { FirebaseSecurityRulesError, SecurityRulesErrorCode } from './security-rules-utils';
import * as validator from '../utils/validator';

const RULES_API_URL = 'https://firebaserules.googleapis.com/v1';
const RULES_V1_API = 'https://firebaserules.googleapis.com/v1';

export interface Release {
readonly name: string;
readonly rulesetName: string;
readonly createTime: string;
readonly updateTime: string;
}

export interface RulesetContent {
readonly source: {
readonly files: Array<{name: string, content: string}>;
};
}

export interface RulesetResponse extends RulesetContent {
readonly name: string;
readonly createTime: string;
}

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

constructor(private readonly httpClient: HttpClient, projectId: string) {
if (!validator.isNonNullObject(httpClient)) {
throw new FirebaseSecurityRulesError(
'invalid-argument', 'HttpClient must be a non-null object.');
}

if (!validator.isNonEmptyString(projectId)) {
throw new FirebaseSecurityRulesError(
'invalid-argument',
Expand All @@ -39,7 +62,49 @@ export class SecurityRulesApiClient {
+ 'environment variable.');
}

this.url = `${RULES_API_URL}/projects/${projectId}`;
this.url = `${RULES_V1_API}/projects/${projectId}`;
}

public getRuleset(name: string): Promise<RulesetResponse> {
return Promise.resolve()
.then(() => {
return this.getRulesetName(name);
})
.then((rulesetName) => {
return this.getResource<RulesetResponse>(rulesetName);
});
}

public createRuleset(ruleset: RulesetContent): Promise<RulesetResponse> {
if (!validator.isNonNullObject(ruleset) ||
!validator.isNonNullObject(ruleset.source) ||
!validator.isNonEmptyArray(ruleset.source.files)) {

const err = new FirebaseSecurityRulesError('invalid-argument', 'Invalid rules content.');
return Promise.reject(err);
}

for (const rf of ruleset.source.files) {
if (!validator.isNonNullObject(rf) ||
!validator.isNonEmptyString(rf.name) ||
!validator.isNonEmptyString(rf.content)) {

const err = new FirebaseSecurityRulesError(
'invalid-argument', `Invalid rules file argument: ${JSON.stringify(rf)}`);
return Promise.reject(err);
}
}

const request: HttpRequestConfig = {
method: 'POST',
url: `${this.url}/rulesets`,
data: ruleset,
};
return this.sendRequest<RulesetResponse>(request);
}

public getRelease(name: string): Promise<Release> {
return this.getResource<Release>(`releases/${name}`);
}

/**
Expand All @@ -49,11 +114,29 @@ export class SecurityRulesApiClient {
* @param {string} name Full qualified name of the resource to get.
* @returns {Promise<T>} A promise that fulfills with the resource.
*/
public getResource<T>(name: string): Promise<T> {
private getResource<T>(name: string): Promise<T> {
const request: HttpRequestConfig = {
method: 'GET',
url: `${this.url}/${name}`,
};
return this.sendRequest<T>(request);
}

private getRulesetName(name: string): string {
if (!validator.isNonEmptyString(name)) {
throw new FirebaseSecurityRulesError(
'invalid-argument', 'Ruleset name must be a non-empty string.');
}

if (name.indexOf('/') !== -1) {
throw new FirebaseSecurityRulesError(
'invalid-argument', 'Ruleset name must not contain any "/" characters.');
}

return `rulesets/${name}`;
}

private sendRequest<T>(request: HttpRequestConfig): Promise<T> {
return this.httpClient.send(request)
.then((resp) => {
return resp.data as T;
Expand Down
84 changes: 52 additions & 32 deletions src/security-rules/security-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../
import { FirebaseApp } from '../firebase-app';
import * as utils from '../utils/index';
import * as validator from '../utils/validator';
import { SecurityRulesApiClient } from './security-rules-api-client';
import { SecurityRulesApiClient, RulesetResponse, RulesetContent } from './security-rules-api-client';
import { AuthorizedHttpClient } from '../utils/api-request';
import { FirebaseSecurityRulesError } from './security-rules-utils';

Expand All @@ -38,21 +38,6 @@ export interface RulesetMetadata {
readonly createTime: string;
}

interface Release {
readonly name: string;
readonly rulesetName: string;
readonly createTime: string;
readonly updateTime: string;
}

interface RulesetResponse {
readonly name: string;
readonly createTime: string;
readonly source: {
readonly files: RulesFile[];
};
}

/**
* Represents a set of Firebase security rules.
*/
Expand Down Expand Up @@ -114,20 +99,7 @@ export class SecurityRules implements FirebaseServiceInterface {
* @returns {Promise<Ruleset>} A promise that fulfills with the specified Ruleset.
*/
public getRuleset(name: string): Promise<Ruleset> {
if (!validator.isNonEmptyString(name)) {
const err = new FirebaseSecurityRulesError(
'invalid-argument', 'Ruleset name must be a non-empty string.');
return Promise.reject(err);
}

if (name.indexOf('/') !== -1) {
const err = new FirebaseSecurityRulesError(
'invalid-argument', 'Ruleset name must not contain any "/" characters.');
return Promise.reject(err);
}

const resource = `rulesets/${name}`;
return this.client.getResource<RulesetResponse>(resource)
return this.client.getRuleset(name)
.then((rulesetResponse) => {
return new Ruleset(rulesetResponse);
});
Expand All @@ -143,9 +115,57 @@ export class SecurityRules implements FirebaseServiceInterface {
return this.getRulesetForRelease(SecurityRules.CLOUD_FIRESTORE);
}

/**
* Creates a `RulesFile` with the given name and source. Throws if any of the arguments are invalid. This is a
* local operation, and does not involve any network API calls.
*
* @param {string} name Name to assign to the rules file.
* @param {string|Buffer} source Contents of the rules file.
* @returns {RulesFile} A new rules file instance.
*/
public createRulesFileFromSource(name: string, source: string | Buffer): RulesFile {
if (!validator.isNonEmptyString(name)) {
throw new FirebaseSecurityRulesError(
'invalid-argument', 'Name must be a non-empty string.');
}

let content: string;
if (validator.isNonEmptyString(source)) {
content = source;
} else if (validator.isBuffer(source)) {
content = source.toString('utf-8');
} else {
throw new FirebaseSecurityRulesError(
'invalid-argument', 'Source must be a non-empty string or a Buffer.');
}

return {
name,
content,
};
}

/**
* Creates a new `Ruleset` from the given `RulesFile`.
*
* @param {RulesFile} file Rules file to include in the new Ruleset.
* @returns {Promise<Ruleset>} A promise that fulfills with the newly created Ruleset.
*/
public createRuleset(file: RulesFile): Promise<Ruleset> {
const ruleset: RulesetContent = {
source: {
files: [ file ],
},
};

return this.client.createRuleset(ruleset)
.then((rulesetResponse) => {
return new Ruleset(rulesetResponse);
});
}

private getRulesetForRelease(releaseName: string): Promise<Ruleset> {
const resource = `releases/${releaseName}`;
return this.client.getResource<Release>(resource)
return this.client.getRelease(releaseName)
.then((release) => {
const rulesetName = release.rulesetName;
if (!validator.isNonEmptyString(rulesetName)) {
Expand Down
Loading

0 comments on commit 471aa49

Please sign in to comment.