-
Notifications
You must be signed in to change notification settings - Fork 407
Implementing the Firebase Security Rules API #604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
9b1a25a
c57c382
0f6fd73
d58f009
fe16838
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| /*! | ||
| * Copyright 2019 Google Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| import { HttpRequestConfig, HttpClient, HttpError } from '../utils/api-request'; | ||
| import { PrefixedFirebaseError } from '../utils/error'; | ||
| import { FirebaseSecurityRulesError, SecurityRulesErrorCode } from './security-rules-utils'; | ||
|
|
||
| const RULES_API_URL = 'https://firebaserules.googleapis.com/v1/'; | ||
|
|
||
| /** | ||
| * Class that facilitates sending requests to the Firebase security rules backend API. | ||
| * | ||
| * @private | ||
| */ | ||
| export class SecurityRulesApiClient { | ||
|
|
||
| constructor(private readonly httpClient: HttpClient) { } | ||
|
|
||
| /** | ||
| * Gets the specified resource from the rules API. Resource name must be full qualified names with project | ||
| * ID prefix (e.g. `projects/project-id/rulesets/ruleset-name`). | ||
| * | ||
| * @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> { | ||
| if (!name.startsWith('projects/')) { | ||
|
||
| const err = new FirebaseSecurityRulesError( | ||
| 'invalid-argument', 'Resource name must have a project ID prefix.'); | ||
| return Promise.reject(err); | ||
| } | ||
|
|
||
| const request: HttpRequestConfig = { | ||
| method: 'GET', | ||
| url: `${RULES_API_URL}${name}`, | ||
| }; | ||
| return this.httpClient.send(request) | ||
| .then((resp) => { | ||
| return resp.data as T; | ||
ryanpbrewster marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }) | ||
| .catch((err) => { | ||
| throw this.toFirebaseError(err); | ||
| }); | ||
| } | ||
|
|
||
| private toFirebaseError(err: HttpError): PrefixedFirebaseError { | ||
| if (err instanceof PrefixedFirebaseError) { | ||
| return err; | ||
| } | ||
|
|
||
| const response = err.response; | ||
| if (!response.isJson()) { | ||
| return new FirebaseSecurityRulesError( | ||
| 'unknown-error', | ||
| `Unexpected response with status: ${response.status} and body: ${response.text}`); | ||
| } | ||
|
|
||
| const error: Error = (response.data as ErrorResponse).error || {}; | ||
| const code = ERROR_CODE_MAPPING[error.status] || 'unknown-error'; | ||
| const message = error.message || `Unknown server error: ${response.text}`; | ||
| return new FirebaseSecurityRulesError(code, message); | ||
| } | ||
| } | ||
|
|
||
| interface ErrorResponse { | ||
| error?: Error; | ||
| } | ||
|
|
||
| interface Error { | ||
| code?: number; | ||
| message?: string; | ||
| status?: string; | ||
| } | ||
|
|
||
| const ERROR_CODE_MAPPING: {[key: string]: SecurityRulesErrorCode} = { | ||
| NOT_FOUND: 'not-found', | ||
| UNAUTHENTICATED: 'authentication-error', | ||
| UNKNOWN: 'unknown-error', | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /*! | ||
| * Copyright 2019 Google Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| import { PrefixedFirebaseError } from '../utils/error'; | ||
|
|
||
| export type SecurityRulesErrorCode = | ||
| 'already-exists' | ||
| | 'authentication-error' | ||
| | 'internal-error' | ||
| | 'invalid-argument' | ||
| | 'invalid-server-response' | ||
| | 'not-found' | ||
| | 'service-unavailable' | ||
| | 'unknown-error'; | ||
|
|
||
| export class FirebaseSecurityRulesError extends PrefixedFirebaseError { | ||
| constructor(code: SecurityRulesErrorCode, message: string) { | ||
| super('security-rules', code, message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| /*! | ||
| * Copyright 2019 Google Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; | ||
| 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 { AuthorizedHttpClient } from '../utils/api-request'; | ||
| import { FirebaseSecurityRulesError } from './security-rules-utils'; | ||
|
|
||
| /** | ||
| * A source file containing some Firebase security rules. | ||
| */ | ||
| export interface RulesFile { | ||
| readonly name: string; | ||
| readonly content: string; | ||
| } | ||
|
|
||
| /** | ||
| * Additional metadata associated with a Ruleset. | ||
| */ | ||
| export interface RulesetMetadata { | ||
| readonly name: string; | ||
| 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[]; | ||
| }; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are part of the internal implementation. They are not exported and also won't be added to the typings. |
||
|
|
||
| /** | ||
| * Representa a set of Firebase security rules. | ||
|
||
| */ | ||
| export class Ruleset implements RulesetMetadata { | ||
|
|
||
| public readonly name: string; | ||
| public readonly createTime: string; | ||
| public readonly source: RulesFile[]; | ||
|
|
||
| constructor(ruleset: RulesetResponse) { | ||
| if (!validator.isNonNullObject(ruleset) || | ||
| !validator.isNonEmptyString(ruleset.name) || | ||
| !validator.isNonEmptyString(ruleset.createTime) || | ||
| !validator.isNonNullObject(ruleset.source)) { | ||
| throw new FirebaseSecurityRulesError( | ||
| 'invalid-argument', | ||
| `Invalid Ruleset response: ${JSON.stringify(ruleset)}`); | ||
| } | ||
|
|
||
| this.name = stripProjectIdPrefix(ruleset.name); | ||
| this.createTime = new Date(ruleset.createTime).toUTCString(); | ||
| this.source = ruleset.source.files || []; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * SecurityRules service bound to the provided app. | ||
| */ | ||
| export class SecurityRules implements FirebaseServiceInterface { | ||
|
|
||
| private static readonly CLOUD_FIRESTORE = 'cloud.firestore'; | ||
|
|
||
| public readonly INTERNAL = new SecurityRulesInternals(); | ||
|
|
||
| private readonly client: SecurityRulesApiClient; | ||
| private readonly projectId: string; | ||
|
|
||
| /** | ||
| * @param {object} app The app for this SecurityRules service. | ||
| * @constructor | ||
| */ | ||
| constructor(readonly app: FirebaseApp) { | ||
| if (!validator.isNonNullObject(app) || !('options' in app)) { | ||
| throw new FirebaseSecurityRulesError( | ||
| 'invalid-argument', | ||
| 'First argument passed to admin.securityRules() must be a valid Firebase app ' | ||
| + 'instance.'); | ||
| } | ||
|
|
||
| const projectId = utils.getProjectId(app); | ||
| if (!validator.isNonEmptyString(projectId)) { | ||
| throw new FirebaseSecurityRulesError( | ||
| 'invalid-argument', | ||
| 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' | ||
| + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT ' | ||
| + 'environment variable.'); | ||
| } | ||
|
|
||
| this.projectId = projectId; | ||
| this.client = new SecurityRulesApiClient(new AuthorizedHttpClient(app)); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the Ruleset identified by the given name. The input name should be the short name string without | ||
| * the project ID prefix. Rejects with a `not-found` error if the specified Ruleset cannot be found. | ||
ryanpbrewster marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * | ||
| * @param {string} name Name of the Ruleset to retrieve. | ||
| * @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 = `projects/${this.projectId}/rulesets/${name}`; | ||
| return this.client.getResource<RulesetResponse>(resource) | ||
| .then((rulesetResponse) => { | ||
| return new Ruleset(rulesetResponse); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the Ruleset currently applied to Cloud Firestore. Rejects with a `not-found` error if no Ruleset is | ||
| * applied on Firestore. | ||
| * | ||
| * @returns {Promise<Ruleset>} A promise that fulfills with the Firestore Ruleset. | ||
| */ | ||
| public getFirestoreRuleset(): Promise<Ruleset> { | ||
| return this.getRulesetForService(SecurityRules.CLOUD_FIRESTORE); | ||
| } | ||
|
|
||
| private getRulesetForService(name: string): Promise<Ruleset> { | ||
ryanpbrewster marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const resource = `projects/${this.projectId}/releases/${name}`; | ||
hiranya911 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return this.client.getResource<Release>(resource) | ||
| .then((release) => { | ||
| const rulesetName = release.rulesetName; | ||
| if (!validator.isNonEmptyString(rulesetName)) { | ||
| throw new FirebaseSecurityRulesError( | ||
| 'not-found', `Ruleset name not found for ${name}.`); | ||
| } | ||
|
|
||
| return this.getRuleset(stripProjectIdPrefix(rulesetName)); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| class SecurityRulesInternals implements FirebaseServiceInternalsInterface { | ||
| public delete(): Promise<void> { | ||
| return Promise.resolve(); | ||
| } | ||
| } | ||
|
|
||
| function stripProjectIdPrefix(name: string): string { | ||
| return name.split('/').pop(); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.