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 3 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
- Improves support for prerelease versions in `ext:dev:publish` (#4244).
- Fixes console error on large uploads to Storage Emulator (#4407).
- Fixes cross-platform incompatibility with Storage Emulator exports (#4411).
- Fixes issue where function deployment errored on projects without secrets (#4425).
- Adds a blocking trigger type (#4395).
31 changes: 27 additions & 4 deletions src/deploy/functions/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,36 @@ export async function prepare(
inferDetailsFromExisting(wantBackend, haveBackend, usedDotenv);
await ensureTriggerRegions(wantBackend);
validate.endpointsAreValid(wantBackend);
inferBlockingDetails(wantBackend);

payload.functions = { wantBackend: wantBackend, haveBackend: haveBackend };

await ensureServiceAgentRoles(projectNumber, wantBackend, haveBackend);
inferDetailsFromExisting(wantBackend, haveBackend, usedDotenv);
inferBlockingDetails(wantBackend);
await ensureTriggerRegions(wantBackend);
// ===Phase 4. Enable APIs required by the deploying backend.

// Enable required APIs. This may come implicitly from triggers (e.g. scheduled triggers
// require cloudscheudler and, in v1, require pub/sub), or can eventually come from
// explicit dependencies.
await Promise.all(
Object.values(wantBackend.requiredAPIs).map(({ api }) => {
return ensureApiEnabled.ensure(projectId, api, "functions", /* silent=*/ false);
})
);
// Note: Some of these are premium APIs that require billing to be enabled.
// We'd eventually have to add special error handling for billing APIs, but
// enableCloudBuild is called above and has this special casing already.
if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
const V2_APIS = [
"artifactregistry.googleapis.com",
"run.googleapis.com",
"eventarc.googleapis.com",
"pubsub.googleapis.com",
"storage.googleapis.com",
];
const enablements = V2_APIS.map((api) => {
return ensureApiEnabled.ensure(context.projectId, api, "functions");
});
await Promise.all(enablements);
}

// ===Phase 5. Ask for user prompts for things might warrant user attentions.
// We limit the scope endpoints being deployed.
Expand Down
31 changes: 11 additions & 20 deletions src/deploy/functions/release/fabricator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,13 @@ 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 @@ -169,7 +167,7 @@ export class Fabricator {
assertExhaustive(endpoint.platform);
}

await this.setTrigger(endpoint, false);
await this.setTrigger(endpoint);
}

async updateEndpoint(update: planner.EndpointUpdate, scraper: SourceTokenScraper): Promise<void> {
Expand All @@ -188,7 +186,7 @@ export class Fabricator {
assertExhaustive(update.endpoint.platform);
}

await this.setTrigger(update.endpoint, true);
await this.setTrigger(update.endpoint);
}

async deleteEndpoint(endpoint: backend.Endpoint): Promise<void> {
Expand Down Expand Up @@ -493,7 +491,7 @@ export class Fabricator {

// Set/Delete trigger is responsible for wiring up a function with any trigger not owned
// by the GCF API. This includes schedules, task queues, and blocking function triggers.
async setTrigger(endpoint: backend.Endpoint, update: boolean): Promise<void> {
async setTrigger(endpoint: backend.Endpoint): Promise<void> {
if (backend.isScheduleTriggered(endpoint)) {
if (endpoint.platform === "gcfv1") {
await this.upsertScheduleV1(endpoint);
Expand All @@ -506,7 +504,7 @@ export class Fabricator {
} else if (backend.isTaskQueueTriggered(endpoint)) {
await this.upsertTaskQueue(endpoint);
} else if (backend.isBlockingTriggered(endpoint)) {
await this.registerBlockingTrigger(endpoint, update);
await this.registerBlockingTrigger(endpoint);
}
}

Expand Down Expand Up @@ -557,15 +555,11 @@ export class Fabricator {
}

async registerBlockingTrigger(
endpoint: backend.Endpoint & backend.BlockingTriggered,
update: boolean
endpoint: backend.Endpoint & backend.BlockingTriggered
): Promise<void> {
this.triggerQueue = this.triggerQueue.then(async () => {
await this.executor
.run(() => services.serviceForEndpoint(endpoint).registerTrigger(endpoint, update))
.catch(rethrowAs(endpoint, "register blocking trigger"));
});
return this.triggerQueue;
await this.executor
.run(() => services.serviceForEndpoint(endpoint).registerTrigger(endpoint))
.catch(rethrowAs(endpoint, "register blocking trigger"));
}

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

logOpStart(op: string, endpoint: backend.Endpoint): void {
Expand Down
218 changes: 122 additions & 96 deletions src/deploy/functions/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,122 +3,148 @@ import * as identityPlatform from "../../../gcp/identityPlatform";
import * as events from "../../../functions/events";
import { FirebaseError } from "../../../error";
import { cloneDeep } from "../../../utils";
import { Name, noop, Service } from "./index";

/**
* 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 validateBlockingTrigger(
endpoint: backend.Endpoint & backend.BlockingTriggered,
wantBackend: backend.Backend
): void {
const blockingEndpoints = backend
.allEndpoints(wantBackend)
.filter((ep) => backend.isBlockingTriggered(ep)) as (backend.Endpoint &
backend.BlockingTriggered)[];
if (
blockingEndpoints.find(
(ep) =>
ep.blockingTrigger.eventType === endpoint.blockingTrigger.eventType && ep.id !== endpoint.id
)
) {
throw new FirebaseError(
`Can only create at most one Auth Blocking Trigger for ${endpoint.blockingTrigger.eventType} events`
);
export class AuthBlockingService implements Service {
name: Name;
api: string;
triggerQueue: Promise<void>;
unregisterQueue: Promise<void>;
Copy link
Member

Choose a reason for hiding this comment

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

nit: unused var

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


constructor() {
this.name = "authblocking";
this.api = "identitytoolkit.googleapis.com";
this.triggerQueue = Promise.resolve();
this.unregisterQueue = Promise.resolve();
this.ensureTriggerRegion = noop;
}
}

function configChanged(
newConfig: identityPlatform.BlockingFunctionsConfig,
config: identityPlatform.BlockingFunctionsConfig
) {
if (
newConfig.triggers?.beforeCreate?.functionUri !== config.triggers?.beforeCreate?.functionUri ||
newConfig.triggers?.beforeSignIn?.functionUri !== config.triggers?.beforeSignIn?.functionUri
) {
return true;
ensureTriggerRegion: (ep: backend.Endpoint & backend.EventTriggered) => Promise<void>;

/**
* 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
*/
validateTrigger(
endpoint: backend.Endpoint & backend.BlockingTriggered,
wantBackend: backend.Backend
): void {
const blockingEndpoints = backend
.allEndpoints(wantBackend)
.filter((ep) => backend.isBlockingTriggered(ep)) as (backend.Endpoint &
backend.BlockingTriggered)[];
if (
blockingEndpoints.find(
(ep) =>
ep.blockingTrigger.eventType === endpoint.blockingTrigger.eventType &&
ep.id !== endpoint.id
)
) {
throw new FirebaseError(
`Can only create at most one Auth Blocking Trigger for ${endpoint.blockingTrigger.eventType} events`
);
}
}
if (
!!newConfig.forwardInboundCredentials?.accessToken !==
!!config.forwardInboundCredentials?.accessToken ||
!!newConfig.forwardInboundCredentials?.idToken !==
!!config.forwardInboundCredentials?.idToken ||
!!newConfig.forwardInboundCredentials?.refreshToken !==
!!config.forwardInboundCredentials?.refreshToken

private configChanged(
newConfig: identityPlatform.BlockingFunctionsConfig,
config: identityPlatform.BlockingFunctionsConfig
) {
return true;
if (
newConfig.triggers?.beforeCreate?.functionUri !==
config.triggers?.beforeCreate?.functionUri ||
newConfig.triggers?.beforeSignIn?.functionUri !== config.triggers?.beforeSignIn?.functionUri
) {
return true;
}
if (
!!newConfig.forwardInboundCredentials?.accessToken !==
!!config.forwardInboundCredentials?.accessToken ||
!!newConfig.forwardInboundCredentials?.idToken !==
!!config.forwardInboundCredentials?.idToken ||
!!newConfig.forwardInboundCredentials?.refreshToken !==
!!config.forwardInboundCredentials?.refreshToken
) {
return true;
}
return false;
}
return false;
}

/**
* Registers the auth blocking trigger to identity platform. On updates, we don't touch the options.
* @param endpoint the blocking endpoint
* @param update if this registration is an update
*/
export async function registerBlockingTrigger(
endpoint: backend.Endpoint & backend.BlockingTriggered,
update: boolean
): Promise<void> {
const newBlockingConfig = await identityPlatform.getBlockingFunctionsConfig(endpoint.project);
const oldBlockingConfig = cloneDeep(newBlockingConfig);
private async registerTriggerLocked(
endpoint: backend.Endpoint & backend.BlockingTriggered
): Promise<void> {
const newBlockingConfig = await identityPlatform.getBlockingFunctionsConfig(endpoint.project);
const oldBlockingConfig = cloneDeep(newBlockingConfig);

if (endpoint.blockingTrigger.eventType === events.v1.BEFORE_CREATE_EVENT) {
newBlockingConfig.triggers = {
...newBlockingConfig.triggers,
beforeCreate: {
functionUri: endpoint.uri!,
},
};
} else {
newBlockingConfig.triggers = {
...newBlockingConfig.triggers,
beforeSignIn: {
functionUri: endpoint.uri!,
},
};
}
if (endpoint.blockingTrigger.eventType === events.v1.BEFORE_CREATE_EVENT) {
newBlockingConfig.triggers = {
...newBlockingConfig.triggers,
beforeCreate: {
functionUri: endpoint.uri!,
},
};
} else {
newBlockingConfig.triggers = {
...newBlockingConfig.triggers,
beforeSignIn: {
functionUri: endpoint.uri!,
},
};
}

if (!update) {
newBlockingConfig.forwardInboundCredentials = {
idToken: endpoint.blockingTrigger.options?.idToken || false,
accessToken: endpoint.blockingTrigger.options?.accessToken || false,
refreshToken: endpoint.blockingTrigger.options?.refreshToken || false,
Copy link
Member

Choose a reason for hiding this comment

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

I like your intention to preserve OOB changes. I think what I would do is say something like

newBlockingConfig.forwardInboundCredentials = { ...oldBlockingConfig.forwardInboundCredentials, ...endpoint.blockingTrigger.options }

This would keep options that were set out of band unless it was specified in the customer's code. The || false you have should generally be unnecessary because the API server will automatically fill zero values for any missing field.

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

};

if (!this.configChanged(newBlockingConfig, oldBlockingConfig)) {
return;
}

await identityPlatform.setBlockingFunctionsConfig(endpoint.project, newBlockingConfig);
}

if (!configChanged(newBlockingConfig, oldBlockingConfig)) {
return;
/**
* Registers the auth blocking trigger to identity platform.
* @param ep the blocking endpoint
*/
registerTrigger(ep: backend.Endpoint & backend.BlockingTriggered): Promise<void> {
this.triggerQueue = this.triggerQueue.then(() => this.registerTriggerLocked(ep));
Copy link
Contributor

Choose a reason for hiding this comment

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

So.. this is ensuring that we don't have concurrent update auth policy. We can have at most 2 requests, but the second one would short-circuit b/c policy would not have been changed?

I think we could have the same effect by just keeping a variable that says "haveUpdated" or something to make sure we the update IAM policy function just once? But that's a small optimization to save 1 unneeded IAM policy call, so I think this impl is fine.

Copy link
Member

Choose a reason for hiding this comment

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

I pushed Cole to this implementation. The problem is that we update functions concurrently and it's possible that we update the beforeCreate function at the same time as beforeSignIn. Since the process of updating involves reading a whole config, changing a field, and saving a whole config, it's possible that two racing updates will read the same original config, change one field, and only one write will win.

By using a promise chain we guarantee that the racing updates will happen sequentially. Think of it as running inside a mutex.

return this.triggerQueue;
}

await identityPlatform.setBlockingFunctionsConfig(endpoint.project, newBlockingConfig);
}
private async unregisterTriggerLocked(
endpoint: backend.Endpoint & backend.BlockingTriggered
): Promise<void> {
const blockingConfig = await identityPlatform.getBlockingFunctionsConfig(endpoint.project);
if (
endpoint.uri !== blockingConfig.triggers?.beforeCreate?.functionUri &&
endpoint.uri !== blockingConfig.triggers?.beforeSignIn?.functionUri
) {
return;
}

/**
* 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 unregisterBlockingTrigger(
endpoint: backend.Endpoint & backend.BlockingTriggered
): Promise<void> {
const blockingConfig = await identityPlatform.getBlockingFunctionsConfig(endpoint.project);
if (
endpoint.uri !== blockingConfig.triggers?.beforeCreate?.functionUri &&
endpoint.uri !== blockingConfig.triggers?.beforeSignIn?.functionUri
) {
return;
}
// 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;
}

// 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);
}

await identityPlatform.setBlockingFunctionsConfig(endpoint.project, blockingConfig);
/**
* Un-registers the auth blocking trigger from identity platform. If the endpoint uri is not on the resource, we do nothing.
* @param ep the blocking endpoint
*/
unregisterTrigger(ep: backend.Endpoint & backend.BlockingTriggered): Promise<void> {
this.triggerQueue = this.triggerQueue.then(() => this.unregisterTriggerLocked(ep));
return this.triggerQueue;
}
}