Skip to content
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

Add blocking trigger type #4395

Merged
merged 36 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
25f3d62
tinkering with implementations
colerogers Jan 31, 2022
6fe07de
Merge remote-tracking branch 'origin/master' into colerogers.auth-blo…
colerogers Feb 17, 2022
f616332
adding in cache for discovery
colerogers Feb 21, 2022
8566842
adding in skeleton for service object and identity platform api calls
colerogers Feb 25, 2022
6a4b7b9
merging master
colerogers Feb 25, 2022
43b2316
fix conflicts
colerogers Mar 23, 2022
eb016f7
adding in all the service code
colerogers Mar 29, 2022
9ce55af
formatter
colerogers Mar 29, 2022
801cb5f
rename
colerogers Mar 30, 2022
0b22d42
Merge remote-tracking branch 'origin/master' into colerogers.auth-blo…
colerogers Apr 2, 2022
94c4db4
removing the lookup before endpointToFunction & adding v2 stuff
colerogers Apr 2, 2022
b1cbf95
add tests & changelog
colerogers Apr 3, 2022
bee1b93
adding tsdoc comments
colerogers Apr 3, 2022
ed416c8
addressing pr comments
colerogers Apr 4, 2022
a298aad
fixing conflicts
colerogers Apr 4, 2022
62999db
removing the empty object from gcp/identityPlatform
colerogers Apr 5, 2022
e35f6c0
Merge remote-tracking branch 'origin/master' into colerogers.auth-blo…
colerogers Apr 5, 2022
fc47475
added typing & fixing endpointFromFunction to set securityLevel and u…
colerogers Apr 5, 2022
43516bc
removing changelog entries
colerogers Apr 5, 2022
851c3a3
cleaning up & set default endpoint options to false
colerogers Apr 5, 2022
b674320
adding in inferBlockingDetails
colerogers Apr 5, 2022
bd9a0b9
checking if config changed before calling set blocking config
colerogers Apr 5, 2022
7f4fce2
formatter
colerogers Apr 5, 2022
7b69417
cleaning up
colerogers Apr 6, 2022
320d222
Merge remote-tracking branch 'origin/master' into colerogers.auth-blo…
colerogers Apr 6, 2022
fbd7e3e
adding event type if in fab & changing api to match prod
colerogers Apr 7, 2022
4fff5c0
adding serviceForEndpoint in fab setTrigger
colerogers Apr 7, 2022
153bffb
converting to options record & yanking auth blocking events from even…
colerogers Apr 7, 2022
c671094
removing old comments
colerogers Apr 7, 2022
3c95a35
fixing delete blocking function bug
colerogers Apr 8, 2022
9faf7a8
merging master
colerogers Apr 8, 2022
77de23e
adding a promise queue for trigger registration & deletion
colerogers Apr 8, 2022
e758d2b
merge master
colerogers Apr 8, 2022
bb56d25
making auth blocking service a class & moving trigger queue into it
colerogers Apr 12, 2022
a4bfd25
remove comment
colerogers Apr 12, 2022
a2f66cd
merging master
colerogers Apr 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
- Fix URL with wrong host returned in storage resumable upload (#4374).
- Fixes Firestore emulator transaction expiration and reused bug.
- Fixes Firestore emulator deadlock bug. [#2452](https://github.com/firebase/firebase-tools/issues/2452)
- Fixes console error on large uploads to Storage Emulator (#4407).
- Fixes cross-platform incompatibility with Storage Emulator exports (#4411).
- Adds a blocking trigger type (#4395).
10 changes: 3 additions & 7 deletions src/deploy/functions/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,8 @@ export interface TaskQueueTriggered {
}

export interface BlockingTrigger {
eventType:
| typeof events.v1.AUTH_BLOCKING_EVENTS[number]
| typeof events.v2.AUTH_BLOCKING_EVENTS[number];
accessToken?: boolean;
idToken?: boolean;
refreshToken?: boolean;
eventType: string;
colerogers marked this conversation as resolved.
Show resolved Hide resolved
options?: Record<string, any>;
}

export interface BlockingTriggered {
Expand All @@ -158,7 +154,7 @@ export function endpointTriggerType(endpoint: Endpoint): string {
} else if (isTaskQueueTriggered(endpoint)) {
return "taskQueue";
} else if (isBlockingTriggered(endpoint)) {
return "blocking";
return endpoint.blockingTrigger.eventType;
} else {
throw new Error("Unexpected trigger type for endpoint " + JSON.stringify(endpoint));
}
Expand Down
31 changes: 19 additions & 12 deletions src/deploy/functions/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ensureServiceAgentRoles } from "./checkIam";
import { FirebaseError } from "../../error";
import { normalizeAndValidate } from "../../functions/projectConfig";
import { previews } from "../../previews";
import { AUTH_BLOCKING_EVENTS } from "../../functions/events/v1";

function hasUserConfig(config: Record<string, unknown>): boolean {
// "firebase" key is always going to exist in runtime config.
Expand Down Expand Up @@ -275,26 +276,32 @@ function maybeCopyTriggerRegion(wantE: backend.Endpoint, haveE: backend.Endpoint

/** Figures out the blocking endpoint options by taking the OR of every trigger option and reassigning that value back to the endpoint. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this the correct behavior? If I have a beforeUserCreated function I must have the same security options as a beforeUserLoggedIn function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's part of the trigger proposal - http://go/cf3-auth-blocking-triggers

Since these are resource level options, they must be the same. Instead of erroring if a user sets the beforeCreate & beforeSignIn options to different values, we just take the OR of them. This will need to be made very clear in our docs

export function inferBlockingDetails(want: backend.Backend): void {
const blockingEndpoints = backend
const authBlockingEndpoints = backend
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit* technically blockingEndpoints 😛

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually here I do a check in filter to see if the eventType is an Auth Blocking Trigger event

.allEndpoints(want)
.filter((ep) => backend.isBlockingTriggered(ep)) as (backend.Endpoint &
backend.BlockingTriggered)[];
.filter(
(ep) =>
backend.isBlockingTriggered(ep) &&
AUTH_BLOCKING_EVENTS.includes(ep.blockingTrigger.eventType as any)
) as (backend.Endpoint & backend.BlockingTriggered)[];

if (blockingEndpoints.length === 0) {
if (authBlockingEndpoints.length === 0) {
return;
}

let accessToken = false;
let idToken = false;
let refreshToken = false;
for (const blockingEp of blockingEndpoints) {
accessToken ||= !!blockingEp.blockingTrigger.accessToken;
idToken ||= !!blockingEp.blockingTrigger.idToken;
refreshToken ||= !!blockingEp.blockingTrigger.refreshToken;
for (const blockingEp of authBlockingEndpoints) {
accessToken ||= !!blockingEp.blockingTrigger.options?.accessToken;
idToken ||= !!blockingEp.blockingTrigger.options?.idToken;
refreshToken ||= !!blockingEp.blockingTrigger.options?.refreshToken;
}
for (const blockingEp of blockingEndpoints) {
blockingEp.blockingTrigger.accessToken = accessToken;
blockingEp.blockingTrigger.idToken = idToken;
blockingEp.blockingTrigger.refreshToken = refreshToken;
for (const blockingEp of authBlockingEndpoints) {
if (!blockingEp.blockingTrigger.options) {
blockingEp.blockingTrigger.options = {};
}
blockingEp.blockingTrigger.options.accessToken = accessToken;
blockingEp.blockingTrigger.options.idToken = idToken;
blockingEp.blockingTrigger.options.refreshToken = refreshToken;
}
}
47 changes: 34 additions & 13 deletions src/deploy/functions/release/fabricator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import * as reporter from "./reporter";
import * as run from "../../../gcp/run";
import * as scheduler from "../../../gcp/cloudscheduler";
import * as utils from "../../../utils";
import * as authBlocking from "../services/authBlocking";
import * as services from "../services";
import { AUTH_BLOCKING_EVENTS } from "../../../functions/events/v1";

// TODO: Tune this for better performance.
const gcfV1PollerOptions: Omit<poller.OperationPollerOptions, "operationResourceName"> = {
Expand Down Expand Up @@ -66,13 +67,15 @@ export class Fabricator {
sourceUrl: string | undefined;
storage: Record<string, gcfV2.StorageSource> | undefined;
appEngineLocation: string;
triggerQueue: Promise<void>;

constructor(args: FabricatorArgs) {
this.executor = args.executor;
this.functionExecutor = args.functionExecutor;
this.sourceUrl = args.sourceUrl;
this.storage = args.storage;
this.appEngineLocation = args.appEngineLocation;
this.triggerQueue = Promise.resolve();
}

async applyPlan(plan: planner.DeploymentPlan): Promise<reporter.Summary> {
Expand Down Expand Up @@ -250,8 +253,11 @@ export class Fabricator {
})
.catch(rethrowAs(endpoint, "set invoker"));
}
} else if (backend.isBlockingTriggered(endpoint)) {
// Blocking functions should always be public
} else if (
backend.isBlockingTriggered(endpoint) &&
AUTH_BLOCKING_EVENTS.includes(endpoint.blockingTrigger.eventType as any)
) {
// Auth Blocking functions should always be public
await this.executor
.run(async () => {
await gcf.setInvokerCreate(endpoint.project, backend.functionName(endpoint), ["public"]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and elsewhere, it's public because it's an auth blocking function, not because it's a blocking function. Please make the code forwards compatible to other blocking trigger types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a check

if (backend.isBlockingTriggered(endpoint) && AUTH_BLOCKING_EVENTS.includes(endpoint.blockingTrigger.eventType)) { ... }

Expand Down Expand Up @@ -326,8 +332,11 @@ export class Fabricator {
})
.catch(rethrowAs(endpoint, "set invoker"));
}
} else if (backend.isBlockingTriggered(endpoint)) {
// Blocking functions should always be public
} else if (
backend.isBlockingTriggered(endpoint) &&
AUTH_BLOCKING_EVENTS.includes(endpoint.blockingTrigger.eventType as any)
) {
// Auth Blocking functions should always be public
await this.executor
.run(() => run.setInvokerCreate(endpoint.project, serviceName, ["public"]))
.catch(rethrowAs(endpoint, "set invoker"));
Expand Down Expand Up @@ -368,7 +377,10 @@ export class Fabricator {
invoker = endpoint.httpsTrigger.invoker;
} else if (backend.isTaskQueueTriggered(endpoint)) {
invoker = endpoint.taskQueueTrigger.invoker;
} else if (backend.isBlockingTriggered(endpoint)) {
} else if (
backend.isBlockingTriggered(endpoint) &&
AUTH_BLOCKING_EVENTS.includes(endpoint.blockingTrigger.eventType as any)
) {
invoker = ["public"];
}
if (invoker) {
Expand Down Expand Up @@ -411,7 +423,10 @@ export class Fabricator {
invoker = endpoint.httpsTrigger.invoker;
} else if (backend.isTaskQueueTriggered(endpoint)) {
invoker = endpoint.taskQueueTrigger.invoker;
} else if (backend.isBlockingTriggered(endpoint)) {
} else if (
backend.isBlockingTriggered(endpoint) &&
AUTH_BLOCKING_EVENTS.includes(endpoint.blockingTrigger.eventType as any)
) {
invoker = ["public"];
}
if (invoker) {
Expand Down Expand Up @@ -545,9 +560,12 @@ export class Fabricator {
endpoint: backend.Endpoint & backend.BlockingTriggered,
update: boolean
): Promise<void> {
await this.executor
.run(() => authBlocking.registerTrigger(endpoint, update))
.catch(rethrowAs(endpoint, "register blocking trigger"));
this.triggerQueue = this.triggerQueue.then(async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: where do we await on this triggerQueue? I was expecting something to wait until the triggerQueue is cleared in the fabricator.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're checking out an old piece of code, but we're just adding the next task in the queue to the then function. When the current task is fulfilled, the next task will be executed.

await this.executor
.run(() => services.serviceForEndpoint(endpoint).registerTrigger(endpoint, update))
.catch(rethrowAs(endpoint, "register blocking trigger"));
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we make the triggerQueue be a feature of the Auth service? That way we run auth trigger updates in serial but can run other trigger updates in parallel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to avoid defining a class for the Auth Blocking service, but parallelizing makes a lot of sense here, I'll make the change

return this.triggerQueue;
}

async deleteScheduleV1(endpoint: backend.Endpoint & backend.ScheduleTriggered): Promise<void> {
Expand Down Expand Up @@ -580,9 +598,12 @@ export class Fabricator {
async unregisterBlockingTrigger(
endpoint: backend.Endpoint & backend.BlockingTriggered
): Promise<void> {
await this.executor
.run(() => authBlocking.unregisterTrigger(endpoint))
.catch(rethrowAs(endpoint, "unregister blocking trigger"));
this.triggerQueue = this.triggerQueue.then(async () => {
await this.executor
.run(() => services.serviceForEndpoint(endpoint).unregisterTrigger(endpoint))
.catch(rethrowAs(endpoint, "unregister blocking trigger"));
});
return this.triggerQueue;
}

logOpStart(op: string, endpoint: backend.Endpoint): void {
Expand Down
2 changes: 1 addition & 1 deletion src/deploy/functions/release/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export function checkForIllegalUpdate(want: backend.Endpoint, have: backend.Endp
} else if (backend.isTaskQueueTriggered(e)) {
return "a task queue";
} else if (backend.isBlockingTriggered(e)) {
return "a blocking";
return e.blockingTrigger.eventType;
}
// Unfortunately TypeScript isn't like Scala and I can't prove to it
// that all cases have been handled
Expand Down
13 changes: 2 additions & 11 deletions src/deploy/functions/runtimes/discovery/v1alpha1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,18 +201,9 @@ function parseEndpoints(
requireKeys(prefix + ".blockingTrigger", ep.blockingTrigger, "eventType");
assertKeyTypes(prefix + ".blockingTrigger", ep.blockingTrigger, {
eventType: "string",
accessToken: "boolean",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending how thorough you want to be, you can detect the eventType is an auth blocking event type and then do the assertKeyTypes for the inner options object

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up taking out the explicit eventType per your last review. The reason why I didn't compare each option is because other blockingTriggers will have a different set of options

idToken: "boolean",
refreshToken: "boolean",
options: "object",
});
triggered = {
blockingTrigger: {
...ep.blockingTrigger,
accessToken: !!ep.blockingTrigger.accessToken,
idToken: !!ep.blockingTrigger.idToken,
refreshToken: !!ep.blockingTrigger.refreshToken,
},
};
triggered = { blockingTrigger: ep.blockingTrigger };
} else {
throw new FirebaseError(
`Do not recognize trigger type for endpoint ${id}. Try upgrading ` +
Expand Down
24 changes: 9 additions & 15 deletions src/deploy/functions/runtimes/node/parseTriggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ import * as args from "../../args";
import * as runtimes from "../../runtimes";
import * as events from "../../../../functions/events";

type BEFORE_CREATE = typeof events.v1.BEFORE_CREATE_EVENT | typeof events.v2.BEFORE_CREATE_EVENT;

type BEFORE_SIGN_IN = typeof events.v1.BEFORE_SIGN_IN_EVENT | typeof events.v2.BEFORE_SIGN_IN_EVENT;

const TRIGGER_PARSER = path.resolve(__dirname, "./triggerParser.js");

export interface ScheduleRetryConfig {
Expand Down Expand Up @@ -74,9 +70,7 @@ export interface TriggerAnnotation {
};
blockingTrigger?: {
eventType: string;
accessToken?: boolean;
idToken?: boolean;
refreshToken?: boolean;
options?: Record<string, any>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit* I've been preferring unknown over any to have the caller assert the type as needed. Is doing so here going to cause a whole bunch of work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #4443

};
failurePolicy?: {};
schedule?: ScheduleAnnotation;
Expand Down Expand Up @@ -225,16 +219,16 @@ export function addResourcesToBackend(
});
triggered = { scheduleTrigger: annotation.schedule };
} else if (annotation.blockingTrigger) {
want.requiredAPIs.push({
api: "identityplatform.googleapis.com",
reason: "Needed for auth blocking functions.",
});
if (events.v1.AUTH_BLOCKING_EVENTS.includes(annotation.blockingTrigger.eventType as any)) {
want.requiredAPIs.push({
api: "identitytoolkit.googleapis.com",
reason: "Needed for auth blocking functions.",
});
}
triggered = {
blockingTrigger: {
eventType: annotation.blockingTrigger.eventType as BEFORE_CREATE | BEFORE_SIGN_IN,
accessToken: !!annotation.blockingTrigger.accessToken,
idToken: !!annotation.blockingTrigger.idToken,
refreshToken: !!annotation.blockingTrigger.refreshToken,
eventType: annotation.blockingTrigger.eventType,
options: annotation.blockingTrigger.options,
},
};
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import * as backend from "../backend";
import * as identityPlatform from "../../../gcp/identityPlatform";
import * as events from "../../../functions/events";
import { FirebaseError } from "../../../error";

const BEFORE_CREATE = events.v1.BEFORE_CREATE_EVENT || events.v2.BEFORE_CREATE_EVENT;
import { cloneDeep } from "../../../utils";

/**
* Ensure that at most one blocking function of that type exists and merges identity platform options on our backend to deploy.
* @param endpoint the Auth Blocking endpoint
* @param wantBackend the backend we are deploying
*/
export function validateAuthBlockingTrigger(
export function validateBlockingTrigger(
endpoint: backend.Endpoint & backend.BlockingTriggered,
wantBackend: backend.Backend
): void {
Expand Down Expand Up @@ -58,14 +57,14 @@ function configChanged(
* @param endpoint the blocking endpoint
* @param update if this registration is an update
*/
export async function registerTrigger(
export async function registerBlockingTrigger(
endpoint: backend.Endpoint & backend.BlockingTriggered,
update: boolean
): Promise<void> {
const newBlockingConfig = await identityPlatform.getBlockingFunctionsConfig(endpoint.project);
const oldBlockingConfig = { ...newBlockingConfig };
const oldBlockingConfig = cloneDeep(newBlockingConfig);

if (endpoint.blockingTrigger.eventType === BEFORE_CREATE) {
if (endpoint.blockingTrigger.eventType === events.v1.BEFORE_CREATE_EVENT) {
newBlockingConfig.triggers = {
...newBlockingConfig.triggers,
beforeCreate: {
Expand All @@ -83,9 +82,9 @@ export async function registerTrigger(

if (!update) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't think I understand why we only set these fields on function create instead of update.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to preserve out-of-band changes, but I guess if we're re-registering the trigger on updates then we should probably re-register the options too

newBlockingConfig.forwardInboundCredentials = {
idToken: endpoint.blockingTrigger.idToken || false,
accessToken: endpoint.blockingTrigger.accessToken || false,
refreshToken: endpoint.blockingTrigger.refreshToken || false,
idToken: endpoint.blockingTrigger.options?.idToken || false,
accessToken: endpoint.blockingTrigger.options?.accessToken || false,
refreshToken: endpoint.blockingTrigger.options?.refreshToken || false,
};
}

Expand All @@ -100,7 +99,7 @@ export async function registerTrigger(
* Un-registers the auth blocking trigger from identity platform. If the endpoint uri is not on the resource, we do nothing.
* @param endpoint the blocking endpoint
*/
export async function unregisterTrigger(
export async function unregisterBlockingTrigger(
endpoint: backend.Endpoint & backend.BlockingTriggered
): Promise<void> {
const blockingConfig = await identityPlatform.getBlockingFunctionsConfig(endpoint.project);
Expand All @@ -111,16 +110,14 @@ export async function unregisterTrigger(
return;
}

if (endpoint.blockingTrigger.eventType === BEFORE_CREATE) {
blockingConfig.triggers = {
...blockingConfig.triggers,
beforeCreate: {},
};
} else {
blockingConfig.triggers = {
...blockingConfig.triggers,
beforeSignIn: {},
};
// There is a possibility that the user changed the registration on identity platform,
// to prevent 400 errors on every create and/or sign in on the app, we will treat
// the blockingConfig as the source of truth and only delete matching uri's.
if (endpoint.uri === blockingConfig.triggers?.beforeCreate?.functionUri) {
delete blockingConfig.triggers?.beforeCreate;
}
if (endpoint.uri === blockingConfig.triggers?.beforeSignIn?.functionUri) {
delete blockingConfig.triggers?.beforeSignIn;
}

await identityPlatform.setBlockingFunctionsConfig(endpoint.project, blockingConfig);
Expand Down