-
Couldn't load subscription status.
- Fork 408
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 all 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,99 @@ | ||
| /*! | ||
| * 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'; | ||
| import * as validator from '../utils/validator'; | ||
|
|
||
| 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 { | ||
|
|
||
| private readonly url: string; | ||
|
|
||
| constructor(private readonly httpClient: HttpClient, projectId: string) { | ||
| 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.url = `${RULES_API_URL}/projects/${projectId}`; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the specified resource from the rules API. Resource names must be the short names without project | ||
| * ID prefix (e.g. `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> { | ||
| const request: HttpRequestConfig = { | ||
| method: 'GET', | ||
| url: `${this.url}/${name}`, | ||
| }; | ||
| return this.httpClient.send(request) | ||
| .then((resp) => { | ||
| return resp.data as T; | ||
| }) | ||
| .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,169 @@ | ||
| /*! | ||
| * 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. |
||
|
|
||
| /** | ||
| * Represents 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; | ||
|
|
||
| /** | ||
| * @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); | ||
| this.client = new SecurityRulesApiClient(new AuthorizedHttpClient(app), projectId); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the Ruleset identified by the given name. The input name should be the short name string without | ||
| * the project ID prefix. For example, to retrieve the `projects/project-id/rulesets/my-ruleset`, pass the | ||
| * short name "my-ruleset". Rejects with a `not-found` error if the specified Ruleset cannot be found. | ||
| * | ||
| * @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 = `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.getRulesetForRelease(SecurityRules.CLOUD_FIRESTORE); | ||
| } | ||
|
|
||
| private getRulesetForRelease(releaseName: string): Promise<Ruleset> { | ||
| const resource = `releases/${releaseName}`; | ||
| 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 ${releaseName}.`); | ||
| } | ||
|
|
||
| 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.