From c30d1a9963c33fb96b388563e4a10d553e1921d1 Mon Sep 17 00:00:00 2001 From: Jessica He Date: Wed, 5 Feb 2025 15:16:00 -0500 Subject: [PATCH 1/3] introduce dangerouslyAllowSignInWithoutUserInCatalog auth resolver config Signed-off-by: Jessica He --- .changeset/twenty-olives-impress.md | 23 +++++++ docs/auth/identity-resolver.md | 38 +++++++++++- .../config.d.ts | 11 +++- .../package.json | 3 +- .../report.api.md | 5 +- .../src/resolvers.ts | 18 +++++- .../config.d.ts | 6 +- .../package.json | 3 +- .../report.api.md | 5 +- .../src/resolvers.ts | 28 +++++++-- .../package.json | 3 +- .../report.api.md | 5 +- .../src/resolvers.ts | 25 ++++++-- .../config.d.ts | 15 ++++- .../package.json | 3 +- .../report.api.md | 10 ++- .../src/resolvers.ts | 47 +++++++++++--- .../config.d.ts | 21 +++++++ .../package.json | 3 +- .../report.api.md | 5 +- .../src/resolvers.ts | 24 +++++-- .../config.d.ts | 6 +- .../package.json | 3 +- .../report.api.md | 5 +- .../src/resolvers.test.ts | 11 +++- .../src/resolvers.ts | 24 +++++-- .../config.d.ts | 16 ++++- .../package.json | 3 +- .../report.api.md | 10 ++- .../src/resolvers.ts | 47 +++++++++++--- .../config.d.ts | 14 +++-- .../package.json | 3 +- .../report.api.md | 5 +- .../src/resolvers.ts | 20 +++++- .../config.d.ts | 11 +++- .../package.json | 3 +- .../report.api.md | 5 +- .../src/resolvers.ts | 20 +++++- .../config.d.ts | 11 +++- .../package.json | 3 +- .../report.api.md | 5 +- .../src/resolvers.ts | 24 +++++-- .../config.d.ts | 16 ++++- .../package.json | 3 +- .../report.api.md | 10 ++- .../src/resolvers.ts | 47 +++++++++++--- .../config.d.ts | 11 +++- .../package.json | 3 +- .../report.api.md | 5 +- .../src/resolvers.ts | 20 +++++- .../package.json | 3 +- .../report.api.md | 13 ++++ .../src/index.ts | 1 + .../src/resolvers.ts | 23 +++++-- .../config.d.ts | 10 ++- .../package.json | 3 +- .../report.api.md | 7 ++- .../src/module.ts | 2 - .../config.d.ts | 11 +++- .../package.json | 3 +- .../report.api.md | 5 +- .../src/resolvers.ts | 24 +++++-- .../config.d.ts | 11 +++- .../package.json | 3 +- .../report.api.md | 5 +- .../src/resolvers.ts | 20 +++++- .../config.d.ts | 6 +- .../resolvers/CatalogAuthResolverContext.ts | 62 +++++++++++++++---- plugins/auth-node/report.api.md | 16 ++++- .../src/sign-in/commonSignInResolvers.ts | 38 +++++++++--- plugins/auth-node/src/types.ts | 14 +++++ yarn.lock | 16 +++++ 72 files changed, 795 insertions(+), 166 deletions(-) create mode 100644 .changeset/twenty-olives-impress.md diff --git a/.changeset/twenty-olives-impress.md b/.changeset/twenty-olives-impress.md new file mode 100644 index 0000000000..9051cf2e67 --- /dev/null +++ b/.changeset/twenty-olives-impress.md @@ -0,0 +1,23 @@ +--- +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': minor +'@backstage/plugin-auth-backend-module-bitbucket-server-provider': minor +'@backstage/plugin-auth-backend-module-azure-easyauth-provider': minor +'@backstage/plugin-auth-backend-module-oauth2-proxy-provider': minor +'@backstage/plugin-auth-backend-module-vmware-cloud-provider': minor +'@backstage/plugin-auth-backend-module-atlassian-provider': minor +'@backstage/plugin-auth-backend-module-bitbucket-provider': minor +'@backstage/plugin-auth-backend-module-microsoft-provider': minor +'@backstage/plugin-auth-backend-module-onelogin-provider': minor +'@backstage/plugin-auth-backend-module-aws-alb-provider': minor +'@backstage/plugin-auth-backend-module-gcp-iap-provider': minor +'@backstage/plugin-auth-backend-module-github-provider': minor +'@backstage/plugin-auth-backend-module-gitlab-provider': minor +'@backstage/plugin-auth-backend-module-google-provider': minor +'@backstage/plugin-auth-backend-module-oauth2-provider': minor +'@backstage/plugin-auth-backend-module-oidc-provider': minor +'@backstage/plugin-auth-backend-module-okta-provider': minor +'@backstage/plugin-auth-backend': minor +'@backstage/plugin-auth-node': minor +--- + +introduce dangerouslyAllowSignInWithoutUserInCatalog auth resolver config diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 6a59805b0b..0c9b44b51d 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -351,8 +351,12 @@ users to sign in, for example by checking email domains. While populating the catalog with organizational data unlocks more powerful ways to browse your software ecosystem, it might not always be a viable or prioritized option. However, even if you do not have user entities populated in your catalog, you -can still sign in users. As there are currently no built-in sign-in resolvers for -this scenario you will need to implement your own. +can still sign in users. + +##### Custom Sign-in Resolver to bypass user in catalog requirement + +As there are currently no built-in sign-in resolvers for +this scenario you may want to implement your own. Signing in a user that doesn't exist in the catalog is as simple as skipping the catalog lookup step from the above example. Rather than looking up the user, we @@ -446,6 +450,36 @@ return ctx.issueToken({ }); ``` +##### Using the `dangerouslyAllowSignInWithoutUserInCatalog` Option + +Another way to bypass this requirement is to enable the `dangerouslyAllowSignInWithoutUserInCatalog` option for resolvers. +Users will still be authenticated as usual but this config will bypass the check that ensures the user is present in the catalog. +If the user entity is not found in the catalog, a Backstage user token will still be issued based on the identifying information available at the resolver level. + +For example: + +```yaml title="Within the provider configuration" +auth: + providers: + github: + development: + ... + signIn: + resolvers: + - resolver: emailLocalPartMatchingUserEntityName + dangerouslyAllowSignInWithoutUserInCatalog: true +``` + +:::warning +Enabling this option in production poses security risks. +::: + +This option may grant access to unexpected users who haven’t been onboarded into +Backstage. Since there is no user entity to associate with the signed-in user, permissions +may not apply as expected and they will have the same permissions as a guest user. +Careful consideration should be given to the permissions assigned to such users, +particularly when using the permission system. + ## Profile Transforms Similar to a custom sign-in resolver, you can also write a custom profile transform diff --git a/plugins/auth-backend-module-atlassian-provider/config.d.ts b/plugins/auth-backend-module-atlassian-provider/config.d.ts index f6433c123e..2729ad31fd 100644 --- a/plugins/auth-backend-module-atlassian-provider/config.d.ts +++ b/plugins/auth-backend-module-atlassian-provider/config.d.ts @@ -32,12 +32,19 @@ export interface Config { additionalScopes?: string | string[]; signIn?: { resolvers: Array< - | { resolver: 'usernameMatchingUserEntityName' } + | { + resolver: 'usernameMatchingUserEntityName'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index 888414bb9d..3d3ad1db41 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -38,7 +38,8 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "passport": "^0.7.0", - "passport-atlassian-oauth2": "^2.1.0" + "passport-atlassian-oauth2": "^2.1.0", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-atlassian-provider/report.api.md b/plugins/auth-backend-module-atlassian-provider/report.api.md index 7abdf7ee70..c9e184aeac 100644 --- a/plugins/auth-backend-module-atlassian-provider/report.api.md +++ b/plugins/auth-backend-module-atlassian-provider/report.api.md @@ -20,7 +20,10 @@ export const atlassianAuthenticator: OAuthAuthenticator< export namespace atlassianSignInResolvers { const usernameMatchingUserEntityName: SignInResolverFactory< OAuthAuthenticatorResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } diff --git a/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts b/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts index 1f3090bfdd..cbd93e4b4f 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts @@ -20,6 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; +import { z } from 'zod'; /** * Available sign-in resolvers for the Atlassian auth provider. @@ -31,7 +32,12 @@ export namespace atlassianSignInResolvers { * Looks up the user by matching their Atlassian username to the entity name. */ export const usernameMatchingUserEntityName = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async ( info: SignInInfo>, ctx, @@ -43,7 +49,15 @@ export namespace atlassianSignInResolvers { throw new Error(`Atlassian user profile does not contain a username`); } - return ctx.signInWithCatalogUser({ entityRef: { name: id } }); + return ctx.signInWithCatalogUser( + { entityRef: { name: id } }, + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: id } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-aws-alb-provider/config.d.ts b/plugins/auth-backend-module-aws-alb-provider/config.d.ts index 41c84872de..3a5d2d5eca 100644 --- a/plugins/auth-backend-module-aws-alb-provider/config.d.ts +++ b/plugins/auth-backend-module-aws-alb-provider/config.d.ts @@ -46,8 +46,12 @@ export interface Config { | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index 55bff1e1d2..0c8c0b8a57 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -42,7 +42,8 @@ "@backstage/plugin-auth-backend": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "jose": "^5.0.0", - "node-cache": "^5.1.2" + "node-cache": "^5.1.2", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-aws-alb-provider/report.api.md b/plugins/auth-backend-module-aws-alb-provider/report.api.md index 567bff993c..ebb3e8e24a 100644 --- a/plugins/auth-backend-module-aws-alb-provider/report.api.md +++ b/plugins/auth-backend-module-aws-alb-provider/report.api.md @@ -41,7 +41,10 @@ export namespace awsAlbSignInResolvers { const // (undocumented) emailMatchingUserEntityProfileEmail: SignInResolverFactory< AwsAlbResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } ``` diff --git a/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts b/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts index 990c447124..5d04de620f 100644 --- a/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts @@ -19,6 +19,8 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { AwsAlbResult } from './types'; +import { z } from 'zod'; + /** * Available sign-in resolvers for the AWS ALB auth provider. * @@ -27,19 +29,33 @@ import { AwsAlbResult } from './types'; export namespace awsAlbSignInResolvers { export const emailMatchingUserEntityProfileEmail = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async (info: SignInInfo, ctx) => { if (!info.result.fullProfile.emails) { throw new Error( 'Login failed, user profile does not contain an email', ); } - return ctx.signInWithCatalogUser({ - filter: { - kind: ['User'], - 'spec.profile.email': info.result.fullProfile.emails[0].value, + + return ctx.signInWithCatalogUser( + { + filter: { + kind: ['User'], + 'spec.profile.email': info.result.fullProfile.emails[0].value, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: info.result.fullProfile.emails[0].value } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-azure-easyauth-provider/package.json b/plugins/auth-backend-module-azure-easyauth-provider/package.json index 546542c116..9b678ce2c0 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/package.json +++ b/plugins/auth-backend-module-azure-easyauth-provider/package.json @@ -40,7 +40,8 @@ "@types/passport": "^1.0.16", "express": "^4.19.2", "jose": "^5.0.0", - "passport": "^0.7.0" + "passport": "^0.7.0", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-azure-easyauth-provider/report.api.md b/plugins/auth-backend-module-azure-easyauth-provider/report.api.md index 9d337e3f34..717864f422 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/report.api.md +++ b/plugins/auth-backend-module-azure-easyauth-provider/report.api.md @@ -32,7 +32,10 @@ export namespace azureEasyAuthSignInResolvers { const // (undocumented) idMatchingUserEntityAnnotation: SignInResolverFactory< AzureEasyAuthResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } diff --git a/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts b/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts index e9e35420d1..17463123c4 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts @@ -19,11 +19,17 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { AzureEasyAuthResult } from './types'; +import { z } from 'zod'; /** @public */ export namespace azureEasyAuthSignInResolvers { export const idMatchingUserEntityAnnotation = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async (info: SignInInfo, ctx) => { const { fullProfile: { id }, @@ -32,12 +38,19 @@ export namespace azureEasyAuthSignInResolvers { if (!id) { throw new Error('User profile contained no id'); } - - return await ctx.signInWithCatalogUser({ - annotations: { - 'graph.microsoft.com/user-id': id, + return ctx.signInWithCatalogUser( + { + annotations: { + 'graph.microsoft.com/user-id': id, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: id } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-bitbucket-provider/config.d.ts b/plugins/auth-backend-module-bitbucket-provider/config.d.ts index f3d82608bd..56303b5215 100644 --- a/plugins/auth-backend-module-bitbucket-provider/config.d.ts +++ b/plugins/auth-backend-module-bitbucket-provider/config.d.ts @@ -30,12 +30,23 @@ export interface Config { additionalScopes?: string | string[]; signIn?: { resolvers: Array< - | { resolver: 'userIdMatchingUserEntityAnnotation' } + | { + resolver: 'userIdMatchingUserEntityAnnotation'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'usernameMatchingUserEntityAnnotation'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json index 38f31b7473..2df7d654f7 100644 --- a/plugins/auth-backend-module-bitbucket-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -38,7 +38,8 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "passport": "^0.7.0", - "passport-bitbucket-oauth2": "^0.1.2" + "passport-bitbucket-oauth2": "^0.1.2", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-bitbucket-provider/report.api.md b/plugins/auth-backend-module-bitbucket-provider/report.api.md index ceb4141fd9..86cff50a0f 100644 --- a/plugins/auth-backend-module-bitbucket-provider/report.api.md +++ b/plugins/auth-backend-module-bitbucket-provider/report.api.md @@ -24,11 +24,17 @@ export const bitbucketAuthenticator: OAuthAuthenticator< export namespace bitbucketSignInResolvers { const userIdMatchingUserEntityAnnotation: SignInResolverFactory< OAuthAuthenticatorResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; const usernameMatchingUserEntityAnnotation: SignInResolverFactory< OAuthAuthenticatorResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } ``` diff --git a/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts b/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts index f9d834a3a7..1dd9783b98 100644 --- a/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts @@ -20,6 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; +import { z } from 'zod'; /** * Available sign-in resolvers for the Bitbucket auth provider. @@ -32,7 +33,12 @@ export namespace bitbucketSignInResolvers { */ export const userIdMatchingUserEntityAnnotation = createSignInResolverFactory( { - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async ( info: SignInInfo>, ctx, @@ -44,11 +50,19 @@ export namespace bitbucketSignInResolvers { throw new Error('Bitbucket user profile does not contain an ID'); } - return ctx.signInWithCatalogUser({ - annotations: { - 'bitbucket.org/user-id': id, + return ctx.signInWithCatalogUser( + { + annotations: { + 'bitbucket.org/user-id': id, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: id } + : undefined, + }, + ); }; }, }, @@ -59,7 +73,12 @@ export namespace bitbucketSignInResolvers { */ export const usernameMatchingUserEntityAnnotation = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async ( info: SignInInfo>, ctx, @@ -73,11 +92,19 @@ export namespace bitbucketSignInResolvers { ); } - return ctx.signInWithCatalogUser({ - annotations: { - 'bitbucket.org/username': username, + return ctx.signInWithCatalogUser( + { + annotations: { + 'bitbucket.org/username': username, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: username } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts b/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts index 8e63581625..26afb46f09 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts +++ b/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts @@ -29,6 +29,27 @@ export interface Config { clientSecret: string; host: string; callbackUrl?: string; + signIn?: { + resolvers: Array< + | { + resolver: 'userIdMatchingUserEntityAnnotation'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'usernameMatchingUserEntityAnnotation'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailLocalPartMatchingUserEntityName'; + allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + >; + }; sessionDuration?: HumanDuration | string; }; }; diff --git a/plugins/auth-backend-module-bitbucket-server-provider/package.json b/plugins/auth-backend-module-bitbucket-server-provider/package.json index 2d5eca7a89..b57937cb6a 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-server-provider/package.json @@ -37,7 +37,8 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "passport": "^0.7.0", - "passport-oauth2": "^1.6.1" + "passport-oauth2": "^1.6.1", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-bitbucket-server-provider/report.api.md b/plugins/auth-backend-module-bitbucket-server-provider/report.api.md index d5c484f6e3..d9409bd36d 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/report.api.md +++ b/plugins/auth-backend-module-bitbucket-server-provider/report.api.md @@ -27,7 +27,10 @@ export const bitbucketServerAuthenticator: OAuthAuthenticator< export namespace bitbucketServerSignInResolvers { const emailMatchingUserEntityProfileEmail: SignInResolverFactory< OAuthAuthenticatorResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } ``` diff --git a/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts b/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts index dc2ceef7e9..cf6970a6df 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts @@ -19,6 +19,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; +import { z } from 'zod'; /** * Available sign-in resolvers for the Bitbucket Server auth provider. @@ -31,7 +32,12 @@ export namespace bitbucketServerSignInResolvers { */ export const emailMatchingUserEntityProfileEmail = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async ( info: SignInInfo>, ctx, @@ -44,11 +50,19 @@ export namespace bitbucketServerSignInResolvers { ); } - return ctx.signInWithCatalogUser({ - filter: { - 'spec.profile.email': profile.email, + return ctx.signInWithCatalogUser( + { + filter: { + 'spec.profile.email': profile.email, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: profile.email } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts b/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts index 39a4ca3eb6..c4ea052db3 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts @@ -34,8 +34,12 @@ export interface Config { | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; }; diff --git a/plugins/auth-backend-module-cloudflare-access-provider/package.json b/plugins/auth-backend-module-cloudflare-access-provider/package.json index 9f70fe9dbe..5ca22b0465 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/package.json +++ b/plugins/auth-backend-module-cloudflare-access-provider/package.json @@ -39,7 +39,8 @@ "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", - "jose": "^5.0.0" + "jose": "^5.0.0", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/report.api.md b/plugins/auth-backend-module-cloudflare-access-provider/report.api.md index 1547190d39..98c55e80bf 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/report.api.md +++ b/plugins/auth-backend-module-cloudflare-access-provider/report.api.md @@ -52,7 +52,10 @@ export type CloudflareAccessResult = { export namespace cloudflareAccessSignInResolvers { const emailMatchingUserEntityProfileEmail: SignInResolverFactory< CloudflareAccessResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.test.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.test.ts index 27cdfa001c..2ab49c313a 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.test.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.test.ts @@ -37,8 +37,13 @@ describe('resolvers', () => { } satisfies Partial; await resolver(info, context as any); - expect(context.signInWithCatalogUser).toHaveBeenCalledWith({ - filter: { 'spec.profile.email': 'hello@example.com' }, - }); + expect(context.signInWithCatalogUser).toHaveBeenCalledWith( + { + filter: { 'spec.profile.email': 'hello@example.com' }, + }, + { + dangerousEntityRefFallback: undefined, + }, + ); }); }); diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts index ad0b512a65..6ac9fba958 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts @@ -19,6 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { CloudflareAccessResult } from './types'; +import { z } from 'zod'; /** * Available sign-in resolvers for the Cloudflare Access auth provider. @@ -31,7 +32,12 @@ export namespace cloudflareAccessSignInResolvers { */ export const emailMatchingUserEntityProfileEmail = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async (info: SignInInfo, ctx) => { const { profile } = info; @@ -41,11 +47,19 @@ export namespace cloudflareAccessSignInResolvers { ); } - return ctx.signInWithCatalogUser({ - filter: { - 'spec.profile.email': profile.email, + return ctx.signInWithCatalogUser( + { + filter: { + 'spec.profile.email': profile.email, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: profile.email } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts index d99d890b50..572afdb0e6 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts +++ b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts @@ -36,13 +36,23 @@ export interface Config { signIn?: { resolvers: Array< - | { resolver: 'emailMatchingUserEntityAnnotation' } - | { resolver: 'idMatchingUserEntityAnnotation' } + | { + resolver: 'emailMatchingUserEntityAnnotation'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'idMatchingUserEntityAnnotation'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index 27fd8fb47d..2d89dbb9fe 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -42,7 +42,8 @@ "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/types": "workspace:^", - "google-auth-library": "^9.0.0" + "google-auth-library": "^9.0.0", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-gcp-iap-provider/report.api.md b/plugins/auth-backend-module-gcp-iap-provider/report.api.md index 1476694f99..5cad28ac87 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/report.api.md +++ b/plugins/auth-backend-module-gcp-iap-provider/report.api.md @@ -35,11 +35,17 @@ export type GcpIapResult = { export namespace gcpIapSignInResolvers { const emailMatchingUserEntityAnnotation: SignInResolverFactory< GcpIapResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; const idMatchingUserEntityAnnotation: SignInResolverFactory< GcpIapResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts index 557ee8fdfe..383d32cc57 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts @@ -19,6 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { GcpIapResult } from './types'; +import { z } from 'zod'; /** * Available sign-in resolvers for the Google auth provider. @@ -30,7 +31,12 @@ export namespace gcpIapSignInResolvers { * Looks up the user by matching their email to the `google.com/email` annotation. */ export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async (info: SignInInfo, ctx) => { const email = info.result.iapToken.email; @@ -38,11 +44,19 @@ export namespace gcpIapSignInResolvers { throw new Error('Google IAP sign-in result is missing email'); } - return ctx.signInWithCatalogUser({ - annotations: { - 'google.com/email': email, + return ctx.signInWithCatalogUser( + { + annotations: { + 'google.com/email': email, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: email } + : undefined, + }, + ); }; }, }); @@ -51,15 +65,28 @@ export namespace gcpIapSignInResolvers { * Looks up the user by matching their user ID to the `google.com/user-id` annotation. */ export const idMatchingUserEntityAnnotation = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async (info: SignInInfo, ctx) => { const userId = info.result.iapToken.sub.split(':')[1]; - return ctx.signInWithCatalogUser({ - annotations: { - 'google.com/user-id': userId, + return ctx.signInWithCatalogUser( + { + annotations: { + 'google.com/user-id': userId, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: userId } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-github-provider/config.d.ts b/plugins/auth-backend-module-github-provider/config.d.ts index 69abfa0d12..15ed412a6f 100644 --- a/plugins/auth-backend-module-github-provider/config.d.ts +++ b/plugins/auth-backend-module-github-provider/config.d.ts @@ -32,12 +32,18 @@ export interface Config { additionalScopes?: string | string[]; signIn?: { resolvers: Array< - | { resolver: 'usernameMatchingUserEntityName' } | { - resolver: 'emailLocalPartMatchingUserEntityName'; - allowedDomains?: string[]; + resolver: 'usernameMatchingUserEntityName'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'preferredUsernameMatchingUserEntityName'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index 2088c400ad..88a4cca547 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -36,7 +36,8 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", - "passport-github2": "^0.1.12" + "passport-github2": "^0.1.12", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-github-provider/report.api.md b/plugins/auth-backend-module-github-provider/report.api.md index 222305adfd..6bad840d00 100644 --- a/plugins/auth-backend-module-github-provider/report.api.md +++ b/plugins/auth-backend-module-github-provider/report.api.md @@ -24,7 +24,10 @@ export const githubAuthenticator: OAuthAuthenticator< export namespace githubSignInResolvers { const usernameMatchingUserEntityName: SignInResolverFactory< OAuthAuthenticatorResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } ``` diff --git a/plugins/auth-backend-module-github-provider/src/resolvers.ts b/plugins/auth-backend-module-github-provider/src/resolvers.ts index 496080a33c..cc1950e9dd 100644 --- a/plugins/auth-backend-module-github-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-github-provider/src/resolvers.ts @@ -20,6 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; +import { z } from 'zod'; /** * Available sign-in resolvers for the GitHub auth provider. @@ -31,7 +32,12 @@ export namespace githubSignInResolvers { * Looks up the user by matching their GitHub username to the entity name. */ export const usernameMatchingUserEntityName = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async ( info: SignInInfo>, ctx, @@ -43,7 +49,17 @@ export namespace githubSignInResolvers { throw new Error(`GitHub user profile does not contain a username`); } - return ctx.signInWithCatalogUser({ entityRef: { name: userId } }); + return ctx.signInWithCatalogUser( + { + entityRef: { name: userId }, + }, + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: userId } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-gitlab-provider/config.d.ts b/plugins/auth-backend-module-gitlab-provider/config.d.ts index 8d47421255..8cbb253e2c 100644 --- a/plugins/auth-backend-module-gitlab-provider/config.d.ts +++ b/plugins/auth-backend-module-gitlab-provider/config.d.ts @@ -32,12 +32,19 @@ export interface Config { additionalScopes?: string | string[]; signIn?: { resolvers: Array< - | { resolver: 'usernameMatchingUserEntityName' } + | { + resolver: 'usernameMatchingUserEntityName'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index 552cfd726a..b0f11e1e04 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -38,7 +38,8 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "passport": "^0.7.0", - "passport-gitlab2": "^5.0.0" + "passport-gitlab2": "^5.0.0", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-gitlab-provider/report.api.md b/plugins/auth-backend-module-gitlab-provider/report.api.md index 44c5ffb214..cb914deeea 100644 --- a/plugins/auth-backend-module-gitlab-provider/report.api.md +++ b/plugins/auth-backend-module-gitlab-provider/report.api.md @@ -24,7 +24,10 @@ export const gitlabAuthenticator: OAuthAuthenticator< export namespace gitlabSignInResolvers { const usernameMatchingUserEntityName: SignInResolverFactory< OAuthAuthenticatorResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } ``` diff --git a/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts b/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts index 755ed08aa0..b235f7b4bb 100644 --- a/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts @@ -20,6 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; +import { z } from 'zod'; /** * Available sign-in resolvers for the GitLab auth provider. @@ -31,7 +32,12 @@ export namespace gitlabSignInResolvers { * Looks up the user by matching their GitLab username to the entity name. */ export const usernameMatchingUserEntityName = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async ( info: SignInInfo>, ctx, @@ -43,7 +49,17 @@ export namespace gitlabSignInResolvers { throw new Error(`GitLab user profile does not contain a username`); } - return ctx.signInWithCatalogUser({ entityRef: { name: id } }); + return ctx.signInWithCatalogUser( + { + entityRef: { name: id }, + }, + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: id } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-google-provider/config.d.ts b/plugins/auth-backend-module-google-provider/config.d.ts index ec3cd92b31..842635480c 100644 --- a/plugins/auth-backend-module-google-provider/config.d.ts +++ b/plugins/auth-backend-module-google-provider/config.d.ts @@ -32,12 +32,19 @@ export interface Config { additionalScopes?: string | string[]; signIn?: { resolvers: Array< - | { resolver: 'emailMatchingUserEntityAnnotation' } + | { + resolver: 'emailMatchingUserEntityAnnotation'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index 5f974bb9fe..447230cc51 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -41,7 +41,8 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "google-auth-library": "^9.0.0", - "passport-google-oauth20": "^2.0.0" + "passport-google-oauth20": "^2.0.0", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-google-provider/report.api.md b/plugins/auth-backend-module-google-provider/report.api.md index eeefb8e837..f06a3c3355 100644 --- a/plugins/auth-backend-module-google-provider/report.api.md +++ b/plugins/auth-backend-module-google-provider/report.api.md @@ -24,7 +24,10 @@ export const googleAuthenticator: OAuthAuthenticator< export namespace googleSignInResolvers { const emailMatchingUserEntityAnnotation: SignInResolverFactory< OAuthAuthenticatorResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } diff --git a/plugins/auth-backend-module-google-provider/src/resolvers.ts b/plugins/auth-backend-module-google-provider/src/resolvers.ts index b19fc4d49f..c9ba884cfb 100644 --- a/plugins/auth-backend-module-google-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-google-provider/src/resolvers.ts @@ -20,6 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; +import { z } from 'zod'; /** * Available sign-in resolvers for the Google auth provider. @@ -31,7 +32,12 @@ export namespace googleSignInResolvers { * Looks up the user by matching their email to the `google.com/email` annotation. */ export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async ( info: SignInInfo>, ctx, @@ -42,11 +48,19 @@ export namespace googleSignInResolvers { throw new Error('Google profile contained no email'); } - return ctx.signInWithCatalogUser({ - annotations: { - 'google.com/email': profile.email, + return ctx.signInWithCatalogUser( + { + annotations: { + 'google.com/email': profile.email, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: profile.email } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-microsoft-provider/config.d.ts b/plugins/auth-backend-module-microsoft-provider/config.d.ts index 3cfcd45a2a..8ea62ddd86 100644 --- a/plugins/auth-backend-module-microsoft-provider/config.d.ts +++ b/plugins/auth-backend-module-microsoft-provider/config.d.ts @@ -34,13 +34,23 @@ export interface Config { skipUserProfile?: boolean; signIn?: { resolvers: Array< - | { resolver: 'emailMatchingUserEntityAnnotation' } + | { + resolver: 'emailMatchingUserEntityAnnotation'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'userIdMatchingUserEntityAnnotation'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } - | { resolver: 'userIdMatchingUserEntityAnnotation' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index a6666690cc..b744d9199a 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -38,7 +38,8 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "jose": "^5.0.0", - "passport-microsoft": "^1.0.0" + "passport-microsoft": "^1.0.0", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-microsoft-provider/report.api.md b/plugins/auth-backend-module-microsoft-provider/report.api.md index 6aef395d7c..9e37eee771 100644 --- a/plugins/auth-backend-module-microsoft-provider/report.api.md +++ b/plugins/auth-backend-module-microsoft-provider/report.api.md @@ -30,11 +30,17 @@ export const microsoftAuthenticator: OAuthAuthenticator< export namespace microsoftSignInResolvers { const emailMatchingUserEntityAnnotation: SignInResolverFactory< OAuthAuthenticatorResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; const userIdMatchingUserEntityAnnotation: SignInResolverFactory< OAuthAuthenticatorResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } ``` diff --git a/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts b/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts index db7aa4f7f3..5090e26c5a 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts @@ -20,6 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; +import { z } from 'zod'; /** * Available sign-in resolvers for the Microsoft auth provider. @@ -31,7 +32,12 @@ export namespace microsoftSignInResolvers { * Looks up the user by matching their Microsoft email to the email entity annotation. */ export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async ( info: SignInInfo>, ctx, @@ -42,11 +48,19 @@ export namespace microsoftSignInResolvers { throw new Error('Microsoft profile contained no email'); } - return ctx.signInWithCatalogUser({ - annotations: { - 'microsoft.com/email': profile.email, + return ctx.signInWithCatalogUser( + { + annotations: { + 'microsoft.com/email': profile.email, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: profile.email } + : undefined, + }, + ); }; }, }); @@ -55,7 +69,12 @@ export namespace microsoftSignInResolvers { */ export const userIdMatchingUserEntityAnnotation = createSignInResolverFactory( { - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async ( info: SignInInfo>, ctx, @@ -68,11 +87,19 @@ export namespace microsoftSignInResolvers { throw new Error('Microsoft profile contained no id'); } - return ctx.signInWithCatalogUser({ - annotations: { - 'graph.microsoft.com/user-id': id, + return ctx.signInWithCatalogUser( + { + annotations: { + 'graph.microsoft.com/user-id': id, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: id } + : undefined, + }, + ); }; }, }, diff --git a/plugins/auth-backend-module-oauth2-provider/config.d.ts b/plugins/auth-backend-module-oauth2-provider/config.d.ts index ba047e3340..8818211318 100644 --- a/plugins/auth-backend-module-oauth2-provider/config.d.ts +++ b/plugins/auth-backend-module-oauth2-provider/config.d.ts @@ -36,12 +36,19 @@ export interface Config { includeBasicAuth?: boolean; signIn?: { resolvers: Array< - | { resolver: 'usernameMatchingUserEntityName' } + | { + resolver: 'usernameMatchingUserEntityName'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index 73c5bb2943..1d1b9acf41 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -37,7 +37,8 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "passport": "^0.7.0", - "passport-oauth2": "^1.6.1" + "passport-oauth2": "^1.6.1", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-oauth2-provider/report.api.md b/plugins/auth-backend-module-oauth2-provider/report.api.md index 632591ec04..b4b7d6d465 100644 --- a/plugins/auth-backend-module-oauth2-provider/report.api.md +++ b/plugins/auth-backend-module-oauth2-provider/report.api.md @@ -24,7 +24,10 @@ export const oauth2Authenticator: OAuthAuthenticator< export namespace oauth2SignInResolvers { const usernameMatchingUserEntityName: SignInResolverFactory< OAuthAuthenticatorResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } ``` diff --git a/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts b/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts index c77f5afa03..7bf098a308 100644 --- a/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts @@ -20,6 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; +import { z } from 'zod'; /** * Available sign-in resolvers for the oauth2 auth provider. @@ -31,7 +32,12 @@ export namespace oauth2SignInResolvers { * Looks up the user by matching their oauth2 username to the entity name. */ export const usernameMatchingUserEntityName = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async ( info: SignInInfo>, ctx, @@ -43,7 +49,17 @@ export namespace oauth2SignInResolvers { throw new Error(`Oauth2 user profile does not contain a username`); } - return ctx.signInWithCatalogUser({ entityRef: { name: id } }); + return ctx.signInWithCatalogUser( + { + entityRef: { name: id }, + }, + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: id } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/package.json b/plugins/auth-backend-module-oauth2-proxy-provider/package.json index e48c4dd547..bd2362af1a 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/package.json +++ b/plugins/auth-backend-module-oauth2-proxy-provider/package.json @@ -36,7 +36,8 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", - "jose": "^5.0.0" + "jose": "^5.0.0", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/report.api.md b/plugins/auth-backend-module-oauth2-proxy-provider/report.api.md index aa360bc9c5..2efdc52635 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/report.api.md +++ b/plugins/auth-backend-module-oauth2-proxy-provider/report.api.md @@ -6,6 +6,7 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; import { IncomingHttpHeaders } from 'http'; import { ProxyAuthenticator } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; // @public (undocumented) const authModuleOauth2ProxyProvider: BackendFeature; @@ -30,4 +31,16 @@ export type OAuth2ProxyResult = { headers: IncomingHttpHeaders; getHeader(name: string): string | undefined; }; + +// @public (undocumented) +export namespace oauth2ProxySignInResolvers { + const // (undocumented) + forwardedUserMatchingUserEntityName: SignInResolverFactory< + OAuth2ProxyResult, + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined + >; +} ``` diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/src/index.ts b/plugins/auth-backend-module-oauth2-proxy-provider/src/index.ts index b804f666c7..34617d4325 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/src/index.ts +++ b/plugins/auth-backend-module-oauth2-proxy-provider/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ export { authModuleOauth2ProxyProvider as default } from './module'; +export { oauth2ProxySignInResolvers } from './resolvers'; export { oauth2ProxyAuthenticator, OAUTH2_PROXY_JWT_HEADER, diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts b/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts index 99ca8bc156..a31e1958dc 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts @@ -19,6 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { OAuth2ProxyResult } from './types'; +import { z } from 'zod'; /** * @public @@ -26,15 +27,29 @@ import { OAuth2ProxyResult } from './types'; export namespace oauth2ProxySignInResolvers { export const forwardedUserMatchingUserEntityName = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async (info: SignInInfo, ctx) => { const name = info.result.getHeader('x-forwarded-user'); if (!name) { throw new Error('Request did not contain a user'); } - return ctx.signInWithCatalogUser({ - entityRef: { name }, - }); + + return ctx.signInWithCatalogUser( + { + entityRef: { name }, + }, + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-oidc-provider/config.d.ts b/plugins/auth-backend-module-oidc-provider/config.d.ts index 8a1df804e6..c11b9319ec 100644 --- a/plugins/auth-backend-module-oidc-provider/config.d.ts +++ b/plugins/auth-backend-module-oidc-provider/config.d.ts @@ -38,8 +38,16 @@ export interface Config { | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'preferredUsernameMatchingUserEntityName'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index f8fb443093..76e726a474 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -41,7 +41,8 @@ "@backstage/types": "workspace:^", "express": "^4.18.2", "openid-client": "^5.5.0", - "passport": "^0.7.0" + "passport": "^0.7.0", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-oidc-provider/report.api.md b/plugins/auth-backend-module-oidc-provider/report.api.md index 93c000c2ee..11cde22db1 100644 --- a/plugins/auth-backend-module-oidc-provider/report.api.md +++ b/plugins/auth-backend-module-oidc-provider/report.api.md @@ -41,12 +41,17 @@ export namespace oidcSignInResolvers { unknown, | { allowedDomains?: string[] | undefined; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; } | undefined >; const emailMatchingUserEntityProfileEmail: SignInResolverFactory< unknown, - unknown + | { + allowedDomains?: string[] | undefined; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } ``` diff --git a/plugins/auth-backend-module-oidc-provider/src/module.ts b/plugins/auth-backend-module-oidc-provider/src/module.ts index 5680cd8603..5e4daf20c3 100644 --- a/plugins/auth-backend-module-oidc-provider/src/module.ts +++ b/plugins/auth-backend-module-oidc-provider/src/module.ts @@ -16,7 +16,6 @@ import { createBackendModule } from '@backstage/backend-plugin-api'; import { authProvidersExtensionPoint, - commonSignInResolvers, createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; import { oidcAuthenticator } from './authenticator'; @@ -38,7 +37,6 @@ export const authModuleOidcProvider = createBackendModule({ authenticator: oidcAuthenticator, signInResolverFactories: { ...oidcSignInResolvers, - ...commonSignInResolvers, }, }), }); diff --git a/plugins/auth-backend-module-okta-provider/config.d.ts b/plugins/auth-backend-module-okta-provider/config.d.ts index 3f27e805ec..f475a1d693 100644 --- a/plugins/auth-backend-module-okta-provider/config.d.ts +++ b/plugins/auth-backend-module-okta-provider/config.d.ts @@ -34,12 +34,19 @@ export interface Config { additionalScopes?: string | string[]; signIn?: { resolvers: Array< - | { resolver: 'emailMatchingUserEntityAnnotation' } + | { + resolver: 'emailMatchingUserEntityAnnotation'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index a46671d1a1..963ef180a6 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -38,7 +38,8 @@ "@backstage/plugin-auth-node": "workspace:^", "@davidzemon/passport-okta-oauth": "^0.0.5", "express": "^4.18.2", - "passport": "^0.7.0" + "passport": "^0.7.0", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-okta-provider/report.api.md b/plugins/auth-backend-module-okta-provider/report.api.md index a142265100..796e0f20a6 100644 --- a/plugins/auth-backend-module-okta-provider/report.api.md +++ b/plugins/auth-backend-module-okta-provider/report.api.md @@ -24,7 +24,10 @@ export const oktaAuthenticator: OAuthAuthenticator< export namespace oktaSignInResolvers { const emailMatchingUserEntityAnnotation: SignInResolverFactory< OAuthAuthenticatorResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } ``` diff --git a/plugins/auth-backend-module-okta-provider/src/resolvers.ts b/plugins/auth-backend-module-okta-provider/src/resolvers.ts index 8bc3f38f17..b3b2a33493 100644 --- a/plugins/auth-backend-module-okta-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-okta-provider/src/resolvers.ts @@ -20,6 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; +import { z } from 'zod'; /** * Available sign-in resolvers for the Okta auth provider. @@ -32,7 +33,12 @@ export namespace oktaSignInResolvers { */ export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async ( info: SignInInfo>, ctx, @@ -43,11 +49,19 @@ export namespace oktaSignInResolvers { throw new Error('Okta profile contained no email'); } - return ctx.signInWithCatalogUser({ - annotations: { - 'okta.com/email': profile.email, + return ctx.signInWithCatalogUser( + { + annotations: { + 'okta.com/email': profile.email, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: profile.email } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-onelogin-provider/config.d.ts b/plugins/auth-backend-module-onelogin-provider/config.d.ts index 58f471bafb..e8fd6f6749 100644 --- a/plugins/auth-backend-module-onelogin-provider/config.d.ts +++ b/plugins/auth-backend-module-onelogin-provider/config.d.ts @@ -31,12 +31,19 @@ export interface Config { callbackUrl?: string; signIn?: { resolvers: Array< - | { resolver: 'usernameMatchingUserEntityName' } + | { + resolver: 'usernameMatchingUserEntityName'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json index 64c5768c3e..ecc1c03825 100644 --- a/plugins/auth-backend-module-onelogin-provider/package.json +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -38,7 +38,8 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "passport": "^0.7.0", - "passport-onelogin-oauth": "^0.0.1" + "passport-onelogin-oauth": "^0.0.1", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-onelogin-provider/report.api.md b/plugins/auth-backend-module-onelogin-provider/report.api.md index 1991f00e0b..df3345f5ed 100644 --- a/plugins/auth-backend-module-onelogin-provider/report.api.md +++ b/plugins/auth-backend-module-onelogin-provider/report.api.md @@ -24,7 +24,10 @@ export const oneLoginAuthenticator: OAuthAuthenticator< export namespace oneLoginSignInResolvers { const usernameMatchingUserEntityName: SignInResolverFactory< OAuthAuthenticatorResult, - unknown + | { + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; } ``` diff --git a/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts b/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts index 93bfd758cb..4a617589da 100644 --- a/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts @@ -20,6 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; +import { z } from 'zod'; /** * Available sign-in resolvers for the OneLogin auth provider. @@ -31,7 +32,12 @@ export namespace oneLoginSignInResolvers { * Looks up the user by matching their OneLogin username to the entity name. */ export const usernameMatchingUserEntityName = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async ( info: SignInInfo>, ctx, @@ -43,7 +49,17 @@ export namespace oneLoginSignInResolvers { throw new Error(`OneLogin user profile does not contain a username`); } - return ctx.signInWithCatalogUser({ entityRef: { name: id } }); + return ctx.signInWithCatalogUser( + { + entityRef: { name: id }, + }, + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: id } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts index 9886d69a41..235d2ab0f1 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts @@ -32,8 +32,12 @@ export interface Config { | { resolver: 'emailLocalPartMatchingUserEntityName'; allowedDomains?: string[]; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + } + | { + resolver: 'emailMatchingUserEntityProfileEmail'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; } - | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; sessionDuration?: HumanDuration | string; diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts index 5268833d14..e4deaaef33 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -139,19 +139,59 @@ export class CatalogAuthResolverContext implements AuthResolverContext { return { entity: result }; } - async signInWithCatalogUser(query: AuthResolverCatalogUserQuery) { - const { entity } = await this.findCatalogUser(query); + async signInWithCatalogUser( + query: AuthResolverCatalogUserQuery, + options?: { + dangerousEntityRefFallback?: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + }, + ) { + try { + const { entity } = await this.findCatalogUser(query); - const { ownershipEntityRefs } = await this.resolveOwnershipEntityRefs( - entity, - ); + const { ownershipEntityRefs } = await this.resolveOwnershipEntityRefs( + entity, + ); - return await this.tokenIssuer.issueToken({ - claims: { - sub: stringifyEntityRef(entity), - ent: ownershipEntityRefs, - }, - }); + return await this.tokenIssuer.issueToken({ + claims: { + sub: stringifyEntityRef(entity), + ent: ownershipEntityRefs, + }, + }); + } catch (error) { + if (error?.name !== 'NotFoundError') { + throw error; + } + if (!options?.dangerousEntityRefFallback) { + this.logger.error( + 'Failed to sign-in, unable to resolve user identity. For non-production environments, manually provision the user or disable the user provisioning requirement by setting the dangerouslyAllowSignInWithoutUserInCatalog option.', + ); + + throw new Error( + 'Failed to sign-in, unable to resolve user identity. Please verify that your catalog contains the expected User entities that would match your configured sign-in resolver.', + ); + } + + const userEntityRef = stringifyEntityRef( + parseEntityRef(options.dangerousEntityRefFallback, { + defaultKind: 'User', + defaultNamespace: DEFAULT_NAMESPACE, + }), + ); + + return await this.tokenIssuer.issueToken({ + claims: { + sub: userEntityRef, + ent: [userEntityRef], + }, + }); + } } async resolveOwnershipEntityRefs( diff --git a/plugins/auth-node/report.api.md b/plugins/auth-node/report.api.md index ad0d85030c..64716dfe5c 100644 --- a/plugins/auth-node/report.api.md +++ b/plugins/auth-node/report.api.md @@ -110,6 +110,15 @@ export type AuthResolverContext = { }>; signInWithCatalogUser( query: AuthResolverCatalogUserQuery, + options?: { + dangerousEntityRefFallback?: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + }, ): Promise; resolveOwnershipEntityRefs(entity: Entity): Promise<{ ownershipEntityRefs: string[]; @@ -146,12 +155,17 @@ export type ClientAuthResponse = { export namespace commonSignInResolvers { const emailMatchingUserEntityProfileEmail: SignInResolverFactory< unknown, - unknown + | { + allowedDomains?: string[] | undefined; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; + } + | undefined >; const emailLocalPartMatchingUserEntityName: SignInResolverFactory< unknown, | { allowedDomains?: string[] | undefined; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined; } | undefined >; diff --git a/plugins/auth-node/src/sign-in/commonSignInResolvers.ts b/plugins/auth-node/src/sign-in/commonSignInResolvers.ts index 6f0fc7fdb2..307ebbfef5 100644 --- a/plugins/auth-node/src/sign-in/commonSignInResolvers.ts +++ b/plugins/auth-node/src/sign-in/commonSignInResolvers.ts @@ -35,7 +35,13 @@ export namespace commonSignInResolvers { */ export const emailMatchingUserEntityProfileEmail = createSignInResolverFactory({ - create() { + optionsSchema: z + .object({ + allowedDomains: z.array(z.string()).optional(), + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { return async (info, ctx) => { const { profile } = info; @@ -59,11 +65,19 @@ export namespace commonSignInResolvers { const [_, name, _plus, domain] = m; const noPlusEmail = `${name}${domain}`; - return ctx.signInWithCatalogUser({ - filter: { - 'spec.profile.email': noPlusEmail, + return ctx.signInWithCatalogUser( + { + filter: { + 'spec.profile.email': noPlusEmail, + }, }, - }); + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: noPlusEmail } + : undefined, + }, + ); } } // Email had no plus addressing or is missing in the catalog, forward failure @@ -82,6 +96,7 @@ export namespace commonSignInResolvers { optionsSchema: z .object({ allowedDomains: z.array(z.string()).optional(), + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), }) .optional(), create(options = {}) { @@ -102,10 +117,15 @@ export namespace commonSignInResolvers { 'Sign-in user email is not from an allowed domain', ); } - - return ctx.signInWithCatalogUser({ - entityRef: { name: localPart }, - }); + return ctx.signInWithCatalogUser( + { entityRef: { name: localPart } }, + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { name: localPart } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-node/src/types.ts b/plugins/auth-node/src/types.ts index a7202d7748..b628313a4f 100644 --- a/plugins/auth-node/src/types.ts +++ b/plugins/auth-node/src/types.ts @@ -162,10 +162,24 @@ export type AuthResolverContext = { * Finds a single user in the catalog using the provided query, and then * issues an identity for that user using default ownership resolution. * + * If the user is not found, an optional `dangerousEntityRefFallback` + * entity ref can be provided to allow sign-in to proceed by issuing an + * identity based on the given ref. This bypasses the requirement for the + * user to exist in the catalog and should be used with caution. + * * See {@link AuthResolverCatalogUserQuery} for details. */ signInWithCatalogUser( query: AuthResolverCatalogUserQuery, + options?: { + dangerousEntityRefFallback?: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + }, ): Promise; /** diff --git a/yarn.lock b/yarn.lock index a8f9207723..92b7cb5e8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5108,6 +5108,7 @@ __metadata: passport: "npm:^0.7.0" passport-atlassian-oauth2: "npm:^2.1.0" supertest: "npm:^7.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5147,6 +5148,7 @@ __metadata: jose: "npm:^5.0.0" msw: "npm:^2.0.8" node-cache: "npm:^5.1.2" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5165,6 +5167,7 @@ __metadata: express: "npm:^4.19.2" jose: "npm:^5.0.0" passport: "npm:^0.7.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5183,6 +5186,7 @@ __metadata: passport: "npm:^0.7.0" passport-bitbucket-oauth2: "npm:^0.1.2" supertest: "npm:^7.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5201,6 +5205,7 @@ __metadata: passport: "npm:^0.7.0" passport-oauth2: "npm:^1.6.1" supertest: "npm:^7.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5222,6 +5227,7 @@ __metadata: msw: "npm:^2.0.0" node-mocks-http: "npm:^1.0.0" uuid: "npm:^11.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5237,6 +5243,7 @@ __metadata: "@backstage/types": "workspace:^" express: "npm:^4.18.2" google-auth-library: "npm:^9.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5254,6 +5261,7 @@ __metadata: "@types/passport-github2": "npm:^1.2.4" passport-github2: "npm:^0.1.12" supertest: "npm:^7.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5272,6 +5280,7 @@ __metadata: passport: "npm:^0.7.0" passport-gitlab2: "npm:^5.0.0" supertest: "npm:^7.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5289,6 +5298,7 @@ __metadata: google-auth-library: "npm:^9.0.0" passport-google-oauth20: "npm:^2.0.0" supertest: "npm:^7.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5326,6 +5336,7 @@ __metadata: msw: "npm:^1.0.0" passport-microsoft: "npm:^1.0.0" supertest: "npm:^7.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5343,6 +5354,7 @@ __metadata: passport: "npm:^0.7.0" passport-oauth2: "npm:^1.6.1" supertest: "npm:^7.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5356,6 +5368,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" jose: "npm:^5.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5380,6 +5393,7 @@ __metadata: openid-client: "npm:^5.5.0" passport: "npm:^0.7.0" supertest: "npm:^7.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5398,6 +5412,7 @@ __metadata: express: "npm:^4.18.2" passport: "npm:^0.7.0" supertest: "npm:^7.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5416,6 +5431,7 @@ __metadata: passport: "npm:^0.7.0" passport-onelogin-oauth: "npm:^0.0.1" supertest: "npm:^7.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft From 5cc1f7f3eddce661d30fdf0f8f3bdfd3b1a95c11 Mon Sep 17 00:00:00 2001 From: Jessica He Date: Wed, 7 May 2025 15:19:49 +0900 Subject: [PATCH 2/3] Address feedback Signed-off-by: Jessica He --- .changeset/tall-suits-share.md | 23 +++++++++++++ .changeset/twenty-olives-impress.md | 23 ------------- .../config/vocabularies/Backstage/accept.txt | 1 + .../src/resolvers.ts | 2 +- .../src/resolvers.ts | 6 +++- .../src/resolvers.ts | 2 +- .../src/resolvers.ts | 4 +-- .../src/resolvers.ts | 2 +- .../src/resolvers.ts | 2 +- .../src/resolvers.ts | 4 +-- .../src/resolvers.ts | 2 +- .../src/resolvers.ts | 2 +- .../src/resolvers.ts | 2 +- .../src/resolvers.ts | 4 +-- .../src/resolvers.ts | 2 +- .../src/resolvers.ts | 2 +- .../src/resolvers.ts | 2 +- .../src/resolvers.ts | 2 +- .../resolvers/CatalogAuthResolverContext.ts | 33 ++++++++----------- plugins/auth-node/report.api.md | 16 +++++---- .../src/sign-in/commonSignInResolvers.ts | 4 +-- plugins/auth-node/src/types.ts | 16 +++++---- 22 files changed, 80 insertions(+), 76 deletions(-) create mode 100644 .changeset/tall-suits-share.md delete mode 100644 .changeset/twenty-olives-impress.md diff --git a/.changeset/tall-suits-share.md b/.changeset/tall-suits-share.md new file mode 100644 index 0000000000..b7c5d1ccfe --- /dev/null +++ b/.changeset/tall-suits-share.md @@ -0,0 +1,23 @@ +--- +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch +'@backstage/plugin-auth-backend-module-bitbucket-server-provider': patch +'@backstage/plugin-auth-backend-module-azure-easyauth-provider': patch +'@backstage/plugin-auth-backend-module-oauth2-proxy-provider': patch +'@backstage/plugin-auth-backend-module-vmware-cloud-provider': patch +'@backstage/plugin-auth-backend-module-atlassian-provider': patch +'@backstage/plugin-auth-backend-module-bitbucket-provider': patch +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +'@backstage/plugin-auth-backend-module-onelogin-provider': patch +'@backstage/plugin-auth-backend-module-aws-alb-provider': patch +'@backstage/plugin-auth-backend-module-gcp-iap-provider': patch +'@backstage/plugin-auth-backend-module-github-provider': patch +'@backstage/plugin-auth-backend-module-gitlab-provider': patch +'@backstage/plugin-auth-backend-module-google-provider': patch +'@backstage/plugin-auth-backend-module-oauth2-provider': patch +'@backstage/plugin-auth-backend-module-oidc-provider': patch +'@backstage/plugin-auth-backend-module-okta-provider': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-auth-node': patch +--- + +introduce dangerouslyAllowSignInWithoutUserInCatalog auth resolver config diff --git a/.changeset/twenty-olives-impress.md b/.changeset/twenty-olives-impress.md deleted file mode 100644 index 9051cf2e67..0000000000 --- a/.changeset/twenty-olives-impress.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@backstage/plugin-auth-backend-module-cloudflare-access-provider': minor -'@backstage/plugin-auth-backend-module-bitbucket-server-provider': minor -'@backstage/plugin-auth-backend-module-azure-easyauth-provider': minor -'@backstage/plugin-auth-backend-module-oauth2-proxy-provider': minor -'@backstage/plugin-auth-backend-module-vmware-cloud-provider': minor -'@backstage/plugin-auth-backend-module-atlassian-provider': minor -'@backstage/plugin-auth-backend-module-bitbucket-provider': minor -'@backstage/plugin-auth-backend-module-microsoft-provider': minor -'@backstage/plugin-auth-backend-module-onelogin-provider': minor -'@backstage/plugin-auth-backend-module-aws-alb-provider': minor -'@backstage/plugin-auth-backend-module-gcp-iap-provider': minor -'@backstage/plugin-auth-backend-module-github-provider': minor -'@backstage/plugin-auth-backend-module-gitlab-provider': minor -'@backstage/plugin-auth-backend-module-google-provider': minor -'@backstage/plugin-auth-backend-module-oauth2-provider': minor -'@backstage/plugin-auth-backend-module-oidc-provider': minor -'@backstage/plugin-auth-backend-module-okta-provider': minor -'@backstage/plugin-auth-backend': minor -'@backstage/plugin-auth-node': minor ---- - -introduce dangerouslyAllowSignInWithoutUserInCatalog auth resolver config diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index f5defc22f2..67ccf26c28 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -302,6 +302,7 @@ oidc Okta Olausson Oldsberg +onboarded onboarding Onboarding onelogin diff --git a/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts b/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts index cbd93e4b4f..949f25db9c 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts @@ -54,7 +54,7 @@ export namespace atlassianSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: id } + ? { entityRef: { name: id } } : undefined, }, ); diff --git a/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts b/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts index 5d04de620f..38b4b60f27 100644 --- a/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts @@ -52,7 +52,11 @@ export namespace awsAlbSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: info.result.fullProfile.emails[0].value } + ? { + entityRef: { + name: info.result.fullProfile.emails[0].value, + }, + } : undefined, }, ); diff --git a/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts b/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts index 17463123c4..94afa90c5f 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts @@ -47,7 +47,7 @@ export namespace azureEasyAuthSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: id } + ? { entityRef: { name: id } } : undefined, }, ); diff --git a/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts b/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts index 1dd9783b98..6691806e63 100644 --- a/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts @@ -59,7 +59,7 @@ export namespace bitbucketSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: id } + ? { entityRef: { name: id } } : undefined, }, ); @@ -101,7 +101,7 @@ export namespace bitbucketSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: username } + ? { entityRef: { name: username } } : undefined, }, ); diff --git a/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts b/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts index cf6970a6df..2e92d8c6ad 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts @@ -59,7 +59,7 @@ export namespace bitbucketServerSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: profile.email } + ? { entityRef: { name: profile.email } } : undefined, }, ); diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts index 6ac9fba958..21cb124be4 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts @@ -56,7 +56,7 @@ export namespace cloudflareAccessSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: profile.email } + ? { entityRef: { name: profile.email } } : undefined, }, ); diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts index 383d32cc57..77dc7b1062 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts @@ -53,7 +53,7 @@ export namespace gcpIapSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: email } + ? { entityRef: { name: email } } : undefined, }, ); @@ -83,7 +83,7 @@ export namespace gcpIapSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: userId } + ? { entityRef: { name: userId } } : undefined, }, ); diff --git a/plugins/auth-backend-module-github-provider/src/resolvers.ts b/plugins/auth-backend-module-github-provider/src/resolvers.ts index cc1950e9dd..5a6934439f 100644 --- a/plugins/auth-backend-module-github-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-github-provider/src/resolvers.ts @@ -56,7 +56,7 @@ export namespace githubSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: userId } + ? { entityRef: { name: userId } } : undefined, }, ); diff --git a/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts b/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts index b235f7b4bb..d5715f7e91 100644 --- a/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts @@ -56,7 +56,7 @@ export namespace gitlabSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: id } + ? { entityRef: { name: id } } : undefined, }, ); diff --git a/plugins/auth-backend-module-google-provider/src/resolvers.ts b/plugins/auth-backend-module-google-provider/src/resolvers.ts index c9ba884cfb..297ac0da6e 100644 --- a/plugins/auth-backend-module-google-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-google-provider/src/resolvers.ts @@ -57,7 +57,7 @@ export namespace googleSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: profile.email } + ? { entityRef: { name: profile.email } } : undefined, }, ); diff --git a/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts b/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts index 5090e26c5a..0ce276522e 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts @@ -57,7 +57,7 @@ export namespace microsoftSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: profile.email } + ? { entityRef: { name: profile.email } } : undefined, }, ); @@ -96,7 +96,7 @@ export namespace microsoftSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: id } + ? { entityRef: { name: id } } : undefined, }, ); diff --git a/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts b/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts index 7bf098a308..bad3f015c8 100644 --- a/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts @@ -56,7 +56,7 @@ export namespace oauth2SignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: id } + ? { entityRef: { name: id } } : undefined, }, ); diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts b/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts index a31e1958dc..8ac639b3c0 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts @@ -46,7 +46,7 @@ export namespace oauth2ProxySignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name } + ? { entityRef: { name } } : undefined, }, ); diff --git a/plugins/auth-backend-module-okta-provider/src/resolvers.ts b/plugins/auth-backend-module-okta-provider/src/resolvers.ts index b3b2a33493..cdb37dbaae 100644 --- a/plugins/auth-backend-module-okta-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-okta-provider/src/resolvers.ts @@ -58,7 +58,7 @@ export namespace oktaSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: profile.email } + ? { entityRef: { name: profile.email } } : undefined, }, ); diff --git a/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts b/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts index 4a617589da..56710472be 100644 --- a/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts @@ -56,7 +56,7 @@ export namespace oneLoginSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: id } + ? { entityRef: { name: id } } : undefined, }, ); diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts index e4deaaef33..4f9b33d021 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -142,13 +142,15 @@ export class CatalogAuthResolverContext implements AuthResolverContext { async signInWithCatalogUser( query: AuthResolverCatalogUserQuery, options?: { - dangerousEntityRefFallback?: - | string - | { - kind?: string; - namespace?: string; - name: string; - }; + dangerousEntityRefFallback?: { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + }; }, ) { try { @@ -165,21 +167,14 @@ export class CatalogAuthResolverContext implements AuthResolverContext { }, }); } catch (error) { - if (error?.name !== 'NotFoundError') { + if ( + error?.name !== 'NotFoundError' || + !options?.dangerousEntityRefFallback + ) { throw error; } - if (!options?.dangerousEntityRefFallback) { - this.logger.error( - 'Failed to sign-in, unable to resolve user identity. For non-production environments, manually provision the user or disable the user provisioning requirement by setting the dangerouslyAllowSignInWithoutUserInCatalog option.', - ); - - throw new Error( - 'Failed to sign-in, unable to resolve user identity. Please verify that your catalog contains the expected User entities that would match your configured sign-in resolver.', - ); - } - const userEntityRef = stringifyEntityRef( - parseEntityRef(options.dangerousEntityRefFallback, { + parseEntityRef(options.dangerousEntityRefFallback.entityRef, { defaultKind: 'User', defaultNamespace: DEFAULT_NAMESPACE, }), diff --git a/plugins/auth-node/report.api.md b/plugins/auth-node/report.api.md index 64716dfe5c..090f981636 100644 --- a/plugins/auth-node/report.api.md +++ b/plugins/auth-node/report.api.md @@ -111,13 +111,15 @@ export type AuthResolverContext = { signInWithCatalogUser( query: AuthResolverCatalogUserQuery, options?: { - dangerousEntityRefFallback?: - | string - | { - kind?: string; - namespace?: string; - name: string; - }; + dangerousEntityRefFallback?: { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + }; }, ): Promise; resolveOwnershipEntityRefs(entity: Entity): Promise<{ diff --git a/plugins/auth-node/src/sign-in/commonSignInResolvers.ts b/plugins/auth-node/src/sign-in/commonSignInResolvers.ts index 307ebbfef5..2b7442f746 100644 --- a/plugins/auth-node/src/sign-in/commonSignInResolvers.ts +++ b/plugins/auth-node/src/sign-in/commonSignInResolvers.ts @@ -74,7 +74,7 @@ export namespace commonSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: noPlusEmail } + ? { entityRef: { name: noPlusEmail } } : undefined, }, ); @@ -122,7 +122,7 @@ export namespace commonSignInResolvers { { dangerousEntityRefFallback: options?.dangerouslyAllowSignInWithoutUserInCatalog - ? { name: localPart } + ? { entityRef: { name: localPart } } : undefined, }, ); diff --git a/plugins/auth-node/src/types.ts b/plugins/auth-node/src/types.ts index b628313a4f..8d6b8f34fb 100644 --- a/plugins/auth-node/src/types.ts +++ b/plugins/auth-node/src/types.ts @@ -172,13 +172,15 @@ export type AuthResolverContext = { signInWithCatalogUser( query: AuthResolverCatalogUserQuery, options?: { - dangerousEntityRefFallback?: - | string - | { - kind?: string; - namespace?: string; - name: string; - }; + dangerousEntityRefFallback?: { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + }; }, ): Promise; From ab53e6fed475e203791f1654e51d43e1cec52c1f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 13 May 2025 12:03:29 +0200 Subject: [PATCH 3/3] changesets: split changeset for new dangerousEntityRefFallback option Signed-off-by: Patrik Oldsberg --- .changeset/tall-suits-share-auth-backend.md | 5 +++ .changeset/tall-suits-share-auth-node.md | 38 +++++++++++++++++++++ .changeset/tall-suits-share.md | 4 +-- 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 .changeset/tall-suits-share-auth-backend.md create mode 100644 .changeset/tall-suits-share-auth-node.md diff --git a/.changeset/tall-suits-share-auth-backend.md b/.changeset/tall-suits-share-auth-backend.md new file mode 100644 index 0000000000..3f5230c4e3 --- /dev/null +++ b/.changeset/tall-suits-share-auth-backend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added support for the new `dangerousEntityRefFallback` option for `signInWithCatalogUser` in `AuthResolverContext`. diff --git a/.changeset/tall-suits-share-auth-node.md b/.changeset/tall-suits-share-auth-node.md new file mode 100644 index 0000000000..cccd3537f7 --- /dev/null +++ b/.changeset/tall-suits-share-auth-node.md @@ -0,0 +1,38 @@ +--- +'@backstage/plugin-auth-node': patch +--- + +Added a new `dangerousEntityRefFallback` option to the `signInWithCatalogUser` method in `AuthResolverContext`. The option will cause the provided entity reference to be used as a fallback in case the user is not found in the catalog. It is up to the caller to provide the fallback entity reference. + +Auth providers that include pre-defined sign-in resolvers are encouraged to define a flag named `dangerouslyAllowSignInWithoutUserInCatalog` in their config, which in turn enables use of the `dangerousEntityRefFallback` option. For example: + +```ts +export const usernameMatchingUserEntityName = createSignInResolverFactory({ + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { username } = info.result.fullProfile; + if (!username) { + throw new Error('User profile does not contain a username'); + } + + return ctx.signInWithCatalogUser( + { entityRef: { name: username } }, + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { entityRef: { name: username } } + : undefined, + }, + ); + }; + }, +}); +``` diff --git a/.changeset/tall-suits-share.md b/.changeset/tall-suits-share.md index b7c5d1ccfe..05b74bc277 100644 --- a/.changeset/tall-suits-share.md +++ b/.changeset/tall-suits-share.md @@ -16,8 +16,6 @@ '@backstage/plugin-auth-backend-module-oauth2-provider': patch '@backstage/plugin-auth-backend-module-oidc-provider': patch '@backstage/plugin-auth-backend-module-okta-provider': patch -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-auth-node': patch --- -introduce dangerouslyAllowSignInWithoutUserInCatalog auth resolver config +Introduce `dangerouslyAllowSignInWithoutUserInCatalog` auth resolver config.