Skip to content

Commit 52bfaca

Browse files
authored
Implemented IID Delete API (#142)
* Implemented instance ID delete API * Adding a couple of missing new lines * Improved error handling * Improved error handling * Updated test * Fixed indentation; Updated tests * Improved error handling and more unit tests * Cleaned up tests * Using simple string replacement instead of format due to the weird semantics of util.format() * Checking for error code in unit tests; Updated error message format to be consistent across all SDKs.
1 parent 8cde537 commit 52bfaca

16 files changed

+778
-5
lines changed

src/auth/credential.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ export class CertCredential implements Credential {
270270
private createAuthJwt_(): string {
271271
const claims = {
272272
scope: [
273+
'https://www.googleapis.com/auth/cloud-platform',
273274
'https://www.googleapis.com/auth/firebase.database',
274275
'https://www.googleapis.com/auth/firebase.messaging',
275276
'https://www.googleapis.com/auth/identitytoolkit',

src/firebase-app.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {Database} from '@firebase/database';
2929
import {DatabaseService} from './database/database';
3030
import {Firestore} from '@google-cloud/firestore';
3131
import {FirestoreService} from './firestore/firestore';
32+
import {InstanceId} from './instance-id/instance-id';
3233

3334
/**
3435
* Type representing a callback which is called every time an app lifecycle event occurs.
@@ -333,6 +334,17 @@ export class FirebaseApp {
333334
return service.client;
334335
}
335336

337+
/**
338+
* Returns the InstanceId service instance associated with this app.
339+
*
340+
* @return {InstanceId} The InstanceId service instance of this app.
341+
*/
342+
public instanceId(): InstanceId {
343+
return this.ensureService_('iid', () => {
344+
return new InstanceId(this);
345+
});
346+
}
347+
336348
/**
337349
* Returns the name of the FirebaseApp instance.
338350
*

src/firebase-namespace.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {Messaging} from './messaging/messaging';
3030
import {Storage} from './storage/storage';
3131
import {Database} from '@firebase/database';
3232
import {Firestore} from '@google-cloud/firestore';
33+
import {InstanceId} from './instance-id/instance-id';
3334

3435
const DEFAULT_APP_NAME = '[DEFAULT]';
3536

@@ -338,6 +339,18 @@ export class FirebaseNamespace {
338339
return Object.assign(fn, require('@google-cloud/firestore'));
339340
}
340341

342+
/**
343+
* Gets the `InstanceId` service namespace. The returned namespace can be used to get the
344+
* `Instance` service for the default app or an explicitly specified app.
345+
*/
346+
get instanceId(): FirebaseServiceNamespace<InstanceId> {
347+
const ns: FirebaseNamespace = this;
348+
let fn: FirebaseServiceNamespace<InstanceId> = (app?: FirebaseApp) => {
349+
return ns.ensureApp(app).instanceId();
350+
};
351+
return Object.assign(fn, {InstanceId});
352+
}
353+
341354
/**
342355
* Initializes the FirebaseApp instance.
343356
*

src/index.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ declare namespace admin {
5959
function messaging(app?: admin.app.App): admin.messaging.Messaging;
6060
function storage(app?: admin.app.App): admin.storage.Storage;
6161
function firestore(app?: admin.app.App): admin.firestore.Firestore;
62+
function instanceId(app?: admin.app.App): admin.instanceId.InstanceId;
6263
function initializeApp(options: admin.AppOptions, name?: string): admin.app.App;
6364
}
6465

@@ -70,6 +71,7 @@ declare namespace admin.app {
7071
auth(): admin.auth.Auth;
7172
database(url?: string): admin.database.Database;
7273
firestore(): admin.firestore.Firestore;
74+
instanceId(): admin.instanceId.InstanceId;
7375
messaging(): admin.messaging.Messaging;
7476
storage(): admin.storage.Storage;
7577
delete(): Promise<void>;
@@ -416,6 +418,14 @@ declare namespace admin.firestore {
416418
export import setLogFunction = _firestore.setLogFunction;
417419
}
418420

421+
declare namespace admin.instanceId {
422+
interface InstanceId {
423+
app: admin.app.App;
424+
425+
deleteInstanceId(instanceId: string): Promise<void>;
426+
}
427+
}
428+
419429
declare module 'firebase-admin' {
420430
}
421431

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*!
2+
* Copyright 2017 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {FirebaseApp} from '../firebase-app';
18+
import {FirebaseError, FirebaseInstanceIdError, InstanceIdClientErrorCode} from '../utils/error';
19+
import {
20+
HttpMethod, SignedApiRequestHandler, ApiSettings,
21+
} from '../utils/api-request';
22+
23+
import * as validator from '../utils/validator';
24+
25+
/** Firebase IID backend host. */
26+
const FIREBASE_IID_HOST = 'console.firebase.google.com';
27+
/** Firebase IID backend port number. */
28+
const FIREBASE_IID_PORT = 443;
29+
/** Firebase IID backend path. */
30+
const FIREBASE_IID_PATH = '/v1/';
31+
/** Firebase IID request timeout duration in milliseconds. */
32+
const FIREBASE_IID_TIMEOUT = 10000;
33+
34+
/** HTTP error codes raised by the backend server. */
35+
const ERROR_CODES = {
36+
400: 'Malformed instance ID argument.',
37+
401: 'Request not authorized.',
38+
403: 'Project does not match instance ID or the client does not have sufficient privileges.',
39+
404: 'Failed to find the instance ID.',
40+
409: 'Already deleted.',
41+
429: 'Request throttled out by the backend server.',
42+
500: 'Internal server error.',
43+
503: 'Backend servers are over capacity. Try again later.',
44+
};
45+
46+
/**
47+
* Class that provides mechanism to send requests to the Firebase Instance ID backend endpoints.
48+
*/
49+
export class FirebaseInstanceIdRequestHandler {
50+
51+
private host: string = FIREBASE_IID_HOST;
52+
private port: number = FIREBASE_IID_PORT;
53+
private timeout: number = FIREBASE_IID_TIMEOUT;
54+
private signedApiRequestHandler: SignedApiRequestHandler;
55+
private path: string;
56+
57+
/**
58+
* @param {FirebaseApp} app The app used to fetch access tokens to sign API requests.
59+
* @param {string} projectId A Firebase project ID string.
60+
*
61+
* @constructor
62+
*/
63+
constructor(app: FirebaseApp, projectId: string) {
64+
this.signedApiRequestHandler = new SignedApiRequestHandler(app);
65+
this.path = FIREBASE_IID_PATH + `project/${projectId}/instanceId/`;
66+
}
67+
68+
public deleteInstanceId(instanceId: string): Promise<Object> {
69+
if (!validator.isNonEmptyString(instanceId)) {
70+
return Promise.reject(new FirebaseInstanceIdError(
71+
InstanceIdClientErrorCode.INVALID_INSTANCE_ID,
72+
'Instance ID must be a non-empty string.'
73+
));
74+
}
75+
return this.invokeRequestHandler(new ApiSettings(instanceId, 'DELETE'));
76+
}
77+
78+
/**
79+
* Invokes the request handler based on the API settings object passed.
80+
*
81+
* @param {ApiSettings} apiSettings The API endpoint settings to apply to request and response.
82+
* @param {Object} requestData The request data.
83+
* @return {Promise<Object>} A promise that resolves with the response.
84+
*/
85+
private invokeRequestHandler(apiSettings: ApiSettings): Promise<Object> {
86+
let path: string = this.path + apiSettings.getEndpoint();
87+
let httpMethod: HttpMethod = apiSettings.getHttpMethod();
88+
return Promise.resolve()
89+
.then(() => {
90+
return this.signedApiRequestHandler.sendRequest(
91+
this.host, this.port, path, httpMethod, undefined, undefined, this.timeout);
92+
})
93+
.then((response) => {
94+
return response;
95+
})
96+
.catch((response) => {
97+
let error;
98+
if (typeof response === 'object' && 'error' in response) {
99+
error = response.error;
100+
} else {
101+
error = response;
102+
}
103+
104+
if (error instanceof FirebaseError) {
105+
// In case of timeouts and other network errors, the API request handler returns a
106+
// FirebaseError wrapped in the response. Simply throw it here.
107+
throw error;
108+
}
109+
110+
let template: string = ERROR_CODES[response.statusCode];
111+
let message: string;
112+
if (template) {
113+
message = `Instance ID "${apiSettings.getEndpoint()}": ${template}`;
114+
} else {
115+
message = JSON.stringify(error);
116+
}
117+
throw new FirebaseInstanceIdError(InstanceIdClientErrorCode.API_ERROR, message);
118+
});
119+
}
120+
}

src/instance-id/instance-id.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*!
2+
* Copyright 2017 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {FirebaseApp} from '../firebase-app';
18+
import {FirebaseInstanceIdError, InstanceIdClientErrorCode} from '../utils/error';
19+
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
20+
import {FirebaseInstanceIdRequestHandler} from './instance-id-request';
21+
22+
import * as utils from '../utils/index';
23+
import * as validator from '../utils/validator';
24+
25+
/**
26+
* Internals of an InstanceId service instance.
27+
*/
28+
class InstanceIdInternals implements FirebaseServiceInternalsInterface {
29+
/**
30+
* Deletes the service and its associated resources.
31+
*
32+
* @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted.
33+
*/
34+
public delete(): Promise<void> {
35+
// There are no resources to clean up
36+
return Promise.resolve(undefined);
37+
}
38+
}
39+
40+
export class InstanceId implements FirebaseServiceInterface {
41+
public INTERNAL: InstanceIdInternals = new InstanceIdInternals();
42+
43+
private app_: FirebaseApp;
44+
private requestHandler: FirebaseInstanceIdRequestHandler;
45+
46+
/**
47+
* @param {Object} app The app for this InstanceId service.
48+
* @constructor
49+
*/
50+
constructor(app: FirebaseApp) {
51+
if (!validator.isNonNullObject(app) || !('options' in app)) {
52+
throw new FirebaseInstanceIdError(
53+
InstanceIdClientErrorCode.INVALID_ARGUMENT,
54+
'First argument passed to admin.instanceId() must be a valid Firebase app instance.'
55+
);
56+
}
57+
58+
const projectId: string = utils.getProjectId(app);
59+
if (!validator.isNonEmptyString(projectId)) {
60+
// Assert for an explicit projct ID (either via AppOptions or the cert itself).
61+
throw new FirebaseInstanceIdError(
62+
InstanceIdClientErrorCode.INVALID_PROJECT_ID,
63+
'Failed to determine project ID for InstanceId. Initialize the '
64+
+ 'SDK with service account credentials or set project ID as an app option. '
65+
+ 'Alternatively set the GCLOUD_PROJECT environment variable.',
66+
);
67+
}
68+
69+
this.app_ = app;
70+
this.requestHandler = new FirebaseInstanceIdRequestHandler(app, projectId);
71+
}
72+
73+
/**
74+
* Deletes the specified instance ID from Firebase. This can be used to delete an instance ID
75+
* and associated user data from a Firebase project, pursuant to the General Data Protection
76+
* Regulation (GDPR).
77+
*
78+
* @param {string} instanceId The instance ID to be deleted
79+
* @return {Promise<void>} A promise that resolves when the instance ID is successfully deleted.
80+
*/
81+
public deleteInstanceId(instanceId: string): Promise<void> {
82+
return this.requestHandler.deleteInstanceId(instanceId)
83+
.then((result) => {
84+
// Return nothing on success
85+
});
86+
}
87+
88+
/**
89+
* Returns the app associated with this InstanceId instance.
90+
*
91+
* @return {FirebaseApp} The app associated with this InstanceId instance.
92+
*/
93+
get app(): FirebaseApp {
94+
return this.app_;
95+
}
96+
}

src/utils/api-request.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {OutgoingHttpHeaders} from 'http';
2222
import https = require('https');
2323

2424
/** Http method type definition. */
25-
export type HttpMethod = 'GET' | 'POST';
25+
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
2626
/** API callback function type definition. */
2727
export type ApiCallbackFunction = (data: Object) => void;
2828

@@ -224,11 +224,11 @@ export class SignedApiRequestHandler extends HttpRequestHandler {
224224
port: number,
225225
path: string,
226226
httpMethod: HttpMethod,
227-
data: Object,
228-
headers: Object,
229-
timeout: number): Promise<Object> {
227+
data?: Object,
228+
headers?: Object,
229+
timeout?: number): Promise<Object> {
230230
return this.app_.INTERNAL.getToken().then((accessTokenObj) => {
231-
let headersCopy: Object = deepCopy(headers);
231+
let headersCopy: Object = (headers && deepCopy(headers)) || {};
232232
let authorizationHeaderKey = 'Authorization';
233233
headersCopy[authorizationHeaderKey] = 'Bearer ' + accessTokenObj.accessToken;
234234
return super.sendRequest(host, port, path, httpMethod, data, headersCopy, timeout);

src/utils/error.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,21 @@ export class FirebaseFirestoreError extends FirebaseError {
206206
}
207207
}
208208

209+
/**
210+
* Firebase instance ID error code structure. This extends FirebaseError.
211+
*
212+
* @param {ErrorInfo} info The error code info.
213+
* @param {string} [message] The error message. This will override the default
214+
* message if provided.
215+
* @constructor
216+
*/
217+
export class FirebaseInstanceIdError extends FirebaseError {
218+
constructor(info: ErrorInfo, message?: string) {
219+
// Override default message if custom message provided.
220+
super({code: 'instance-id/' + info.code, message: message || info.message});
221+
}
222+
}
223+
209224

210225
/**
211226
* Firebase Messaging error code structure. This extends PrefixedFirebaseError.
@@ -472,6 +487,25 @@ export class MessagingClientErrorCode {
472487
};
473488
};
474489

490+
export class InstanceIdClientErrorCode {
491+
public static INVALID_ARGUMENT = {
492+
code: 'invalid-argument',
493+
message: 'Invalid argument provided.',
494+
};
495+
public static INVALID_PROJECT_ID = {
496+
code: 'invalid-project-id',
497+
message: 'Invalid project ID provided.',
498+
};
499+
public static INVALID_INSTANCE_ID = {
500+
code: 'invalid-instance-id',
501+
message: 'Invalid instance ID provided.',
502+
};
503+
public static API_ERROR = {
504+
code: 'api-error',
505+
message: 'Instance ID API call failed.',
506+
};
507+
}
508+
475509
/** @const {ServerToClientCode} Auth server to client enum error codes. */
476510
const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = {
477511
// Claims payload is too large.

test/integration/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ var utils = require('./utils');
3838
var app = require('./app');
3939
var auth = require('./auth');
4040
var database = require('./database');
41+
var instanceId = require('./instance-id');
4142
var messaging = require('./messaging');
4243
var storage = require('./storage');
4344
var firestore = require('./firestore');
@@ -239,6 +240,7 @@ return promptForUpdateRules(flags['overwrite'])
239240
.then(_.partial(app.test, utils))
240241
.then(_.partial(auth.test, utils))
241242
.then(_.partial(database.test, utils))
243+
.then(_.partial(instanceId.test, utils))
242244
.then(_.partial(messaging.test, utils))
243245
.then(_.partial(storage.test, utils))
244246
.then(_.partial(firestore.test, utils))

0 commit comments

Comments
 (0)