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
Conversation
ensureTriggerIsValid: noop, | ||
copyResourceOptionsToEndpoint: noop, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rant alert - i don't know how to improve this code, but something smells here.
All these noops are making me nervious whether this is a good abstraction.
It's starting to feel like we are stuffing any "do something if this kind of endpoint is given" into this code, and I'm not sure if this is making things clearer for me to understand.
I do like that we have one place to look, but things are starting to look too "abstract" e.g. copyResourceOptionsToEndpoint
didn't really mean anything to me until I read the implementation for Auth blocking function, and I can't think of a reason why this would be relevant to say pubsub function in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After moving the resource options code into it's own function, I was able to remove this Service function property. We're only left with ensureTriggerIsValid
which seems like an okay function to have for every service.
…ri for every httpsTrigger
src/deploy/functions/backend.ts
Outdated
accessToken?: boolean; | ||
idToken?: boolean; | ||
refreshToken?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no suggestion on how to fix this, but I'm worried that this is not scalable. These should be blocking functions, not auth functions. Maybe we should have a map of features or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@inlined How about we go with a record structure for options and for auth blocking triggers we'd assume they'd use these three options. So the trigger will now be
export interface BlockingTrigger {
eventType:
| typeof events.v1.AUTH_BLOCKING_EVENTS[number]
| typeof events.v2.AUTH_BLOCKING_EVENTS[number];
options: Record<string, any>;
}
What do you think? The biggest thing is we'd lose out on is type completion in ts
@@ -271,3 +272,29 @@ function maybeCopyTriggerRegion(wantE: backend.Endpoint, haveE: backend.Endpoint | |||
} | |||
wantE.eventTrigger.region = haveE.eventTrigger.region; | |||
} | |||
|
|||
/** Figures out the blocking endpoint options by taking the OR of every trigger option and reassigning that value back to the endpoint. */ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
// Blocking functions should always be public | ||
await this.executor | ||
.run(async () => { | ||
await gcf.setInvokerCreate(endpoint.project, backend.functionName(endpoint), ["public"]); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) { ... }
src/functions/events/v2.ts
Outdated
export const BEFORE_CREATE_EVENT = "providers/cloud.auth/eventTypes/user.beforeCreate"; | ||
|
||
export const BEFORE_SIGN_IN_EVENT = "providers/cloud.auth/eventTypes/user.beforeSignIn"; | ||
|
||
export const AUTH_BLOCKING_EVENTS = [BEFORE_CREATE_EVENT, BEFORE_SIGN_IN_EVENT] as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IDK if I really love having this defined as a separate event type. I saw some places where only one type is handled and this also gets in the way of GCIP offering a new v2 api (not that they're super inclined to anyway).
In reality, events.v1 and events.v2 isn't about GCF versions as much as it is "eventflow" or "eventarc". It's OK that these event types will be handled by a v2 function and it's possible that we'll be forced to add support for all of events.v1 in v2 functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you recommend just dropping v1 events entirely?
project: string, | ||
blockingConfig: BlockingFunctionsConfig | ||
): Promise<BlockingFunctionsConfig> { | ||
const config = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Would this be easier if you just used proto.fieldMasks
to create your field mask instead of setting it to "blockingFunctions"? Then you wouldn't need to merge both beforeCreate and beforeSignIn before setting it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So funny thing about their API... if you try and use a field mask that has the inner property that changed, like blockingFunctions.triggers.beforeCreate.functionUri
, it will not actually pickup the changes. That's why I went and set it to blockingFunctions
}; | ||
} | ||
|
||
if (!update) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't we modify on updates? Is that not allowed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm preserving out-of-band resource changes. That seemed to fit with our current model, let me know if you disagree
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking really good! I'd personally make the trigger queue more narrowly scoped (e.g. inside auth) so we can parallelize as much as possible and it's not clear to me why we don't update options when the trigger is updated. Otherwise LGTM.
this.triggerQueue = this.triggerQueue.then(async () => { | ||
await this.executor | ||
.run(() => services.serviceForEndpoint(endpoint).registerTrigger(endpoint, update)) | ||
.catch(rethrowAs(endpoint, "register blocking trigger")); | ||
}); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
@@ -201,18 +201,9 @@ function parseEndpoints( | |||
requireKeys(prefix + ".blockingTrigger", ep.blockingTrigger, "eventType"); | |||
assertKeyTypes(prefix + ".blockingTrigger", ep.blockingTrigger, { | |||
eventType: "string", | |||
accessToken: "boolean", |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
@@ -83,9 +82,9 @@ export async function registerTrigger( | |||
|
|||
if (!update) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Few nitpicks, but overall looks good.
@@ -3,6 +3,7 @@ import * as gcf from "../../gcp/cloudfunctions"; | |||
import * as gcfV2 from "../../gcp/cloudfunctionsv2"; | |||
import * as utils from "../../utils"; | |||
import * as runtimes from "./runtimes"; | |||
import * as events from "../../functions/events"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit* remove events per linter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed in #4443
|
||
/** Figures out the blocking endpoint options by taking the OR of every trigger option and reassigning that value back to the endpoint. */ | ||
export function inferBlockingDetails(want: backend.Backend): void { | ||
const authBlockingEndpoints = backend |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit* technically blockingEndpoints
😛
There was a problem hiding this comment.
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
endpoint: backend.Endpoint & backend.BlockingTriggered, | ||
update: boolean | ||
): Promise<void> { | ||
this.triggerQueue = this.triggerQueue.then(async () => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@@ -68,6 +68,10 @@ export interface TriggerAnnotation { | |||
}; | |||
invoker?: string[]; | |||
}; | |||
blockingTrigger?: { | |||
eventType: string; | |||
options?: Record<string, any>; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in #4443
ensureTriggerRegion: (ep: backend.Endpoint & backend.EventTriggered) => Promise<void>; | ||
validateTrigger: ( | ||
ep: backend.Endpoint & backend.BlockingTriggered, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit* This seems unnecessarily strict - in theory, we expect other services to define validateTrigger
no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in #4443
* @param ep the blocking endpoint | ||
*/ | ||
registerTrigger(ep: backend.Endpoint & backend.BlockingTriggered): Promise<void> { | ||
this.triggerQueue = this.triggerQueue.then(() => this.registerTriggerLocked(ep)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@@ -198,6 +199,101 @@ describe("validate", () => { | |||
/they have fewer than 2GB memory/ | |||
); | |||
}); | |||
|
|||
it("Disallows multiple beforeCreate blocking", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit - prefer lowercase disallow
(same comment for tests below).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in #4443
beforeEach(() => { | ||
getConfig = sinon | ||
.stub(identityPlatform, "getBlockingFunctionsConfig") | ||
.throws("Unexpected call to getBlockingFunctionsConfig"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit* for async functions, you'd use reject
. I guess this works since await
is forgiving 🤷🏼
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in #4443
|
||
await authBlockingService.registerTrigger(ep); | ||
|
||
expect(blockingConfig).to.deep.equal(newBlockingConfig); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
huh I'm having a brain freeze, but how does this pass? blockingConfig
and newBlockingConfig
clearly looks different to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I think this is happening because we're mutating return values of blockingConfig? Which seems wrong here... Maybe setConfig
should resolve w/o any return value (unless we depend on it) and we shoudl be verifying that setConfig
was called with newBlockingConfig
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in #4443, we no longer check for a modification to the object
newBlockingConfig.forwardInboundCredentials = { | ||
idToken: endpoint.blockingTrigger.options?.idToken || false, | ||
accessToken: endpoint.blockingTrigger.options?.accessToken || false, | ||
refreshToken: endpoint.blockingTrigger.options?.refreshToken || false, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in #4443
* @param ep the blocking endpoint | ||
*/ | ||
registerTrigger(ep: backend.Endpoint & backend.BlockingTriggered): Promise<void> { | ||
this.triggerQueue = this.triggerQueue.then(() => this.registerTriggerLocked(ep)); |
There was a problem hiding this comment.
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.
name: Name; | ||
api: string; | ||
triggerQueue: Promise<void>; | ||
unregisterQueue: Promise<void>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: unused var
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in #4443
"google.cloud.pubsub.topic.v1.messagePublished": PubSubService, | ||
"google.cloud.storage.object.v1.finalized": StorageService, | ||
"google.cloud.storage.object.v1.archived": StorageService, | ||
"google.cloud.storage.object.v1.deleted": StorageService, | ||
"google.cloud.storage.object.v1.metadataUpdated": StorageService, | ||
"google.firebase.firebasealerts.alerts.v1.published": FirebaseAlertsService, | ||
"providers/cloud.auth/eventTypes/user.beforeCreate": AuthBlockingService, | ||
"providers/cloud.auth/eventTypes/user.beforeSignIn": AuthBlockingService, | ||
"providers/cloud.auth/eventTypes/user.beforeCreate": authBlockingService, | ||
"providers/cloud.auth/eventTypes/user.beforeSignIn": authBlockingService, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the capitalization here confuses me. Are these Classes or variables?
|
||
await authBlockingService.registerTrigger(ep); | ||
|
||
expect(blockingConfig).to.deep.equal(newBlockingConfig); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I think this is happening because we're mutating return values of blockingConfig? Which seems wrong here... Maybe setConfig
should resolve w/o any return value (unless we depend on it) and we shoudl be verifying that setConfig
was called with newBlockingConfig
.
Adds deployment code for blocking trigger types
Requirements: