-
Notifications
You must be signed in to change notification settings - Fork 900
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
Changes from 3 commits
25f3d62
6fe07de
f616332
8566842
6a4b7b9
43b2316
eb016f7
9ce55af
801cb5f
0b22d42
94c4db4
b1cbf95
bee1b93
ed416c8
a298aad
62999db
e35f6c0
fc47475
43516bc
851c3a3
b674320
bd9a0b9
7f4fce2
7b69417
320d222
fbd7e3e
4fff5c0
153bffb
c671094
3c95a35
9faf7a8
77de23e
e758d2b
bb56d25
a4bfd25
a2f66cd
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 |
---|---|---|
|
@@ -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>; | ||
|
||
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, | ||
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. I like your intention to preserve OOB changes. I think what I would do is say something like
This would keep options that were set out of band unless it was specified in the customer's code. The 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. 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)); | ||
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. 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 commentThe 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; | ||
} | ||
} |
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