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 new file mode 100644 index 0000000000..05b74bc277 --- /dev/null +++ b/.changeset/tall-suits-share.md @@ -0,0 +1,21 @@ +--- +'@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 +--- + +Introduce `dangerouslyAllowSignInWithoutUserInCatalog` auth resolver config. diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 0ca8986140..36f9335b1f 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -304,6 +304,7 @@ oidc Okta Olausson Oldsberg +onboarded onboarding Onboarding onelogin diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 3d15c9541e..5fb69c7955 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 e458540b8f..54c109c737 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..949f25db9c 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 + ? { entityRef: { 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 9b98642ab1..c7deb8283d 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..38b4b60f27 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,37 @@ 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 + ? { + entityRef: { + 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 837ed7c9d1..e999bf8895 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..94afa90c5f 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 + ? { entityRef: { 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 55182829cb..d73deac57c 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..6691806e63 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 + ? { entityRef: { 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 + ? { entityRef: { 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 dff0e32895..6653b6c499 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..2e92d8c6ad 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 + ? { entityRef: { 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 3f8e18b5bb..3cdda0a379 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..21cb124be4 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 + ? { entityRef: { 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 7ee3097075..9c0db22b19 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..77dc7b1062 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 + ? { entityRef: { 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 + ? { entityRef: { 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 dabe2c4c5b..4051fc5a2c 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..5a6934439f 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 + ? { entityRef: { 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 fd726ebed5..574a4f05e1 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..d5715f7e91 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 + ? { entityRef: { 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 cdc365ddfb..8faab0b322 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..297ac0da6e 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 + ? { entityRef: { 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 a64b9d04d4..f1dee13ac3 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..0ce276522e 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 + ? { entityRef: { 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 + ? { entityRef: { 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 4b9f9b6c02..65b5171d12 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..bad3f015c8 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 + ? { entityRef: { 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 c1df361682..18c247e1a0 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..8ac639b3c0 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 + ? { entityRef: { 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 65c1d8f208..721a1ab3dd 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 70e528d0b1..9993b32ece 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..cdb37dbaae 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 + ? { entityRef: { 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 1e9efc21ea..7ced50dc61 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..56710472be 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 + ? { entityRef: { 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..4f9b33d021 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -139,19 +139,54 @@ export class CatalogAuthResolverContext implements AuthResolverContext { return { entity: result }; } - async signInWithCatalogUser(query: AuthResolverCatalogUserQuery) { - const { entity } = await this.findCatalogUser(query); + async signInWithCatalogUser( + query: AuthResolverCatalogUserQuery, + options?: { + dangerousEntityRefFallback?: { + entityRef: + | 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' || + !options?.dangerousEntityRefFallback + ) { + throw error; + } + const userEntityRef = stringifyEntityRef( + parseEntityRef(options.dangerousEntityRefFallback.entityRef, { + 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..090f981636 100644 --- a/plugins/auth-node/report.api.md +++ b/plugins/auth-node/report.api.md @@ -110,6 +110,17 @@ export type AuthResolverContext = { }>; signInWithCatalogUser( query: AuthResolverCatalogUserQuery, + options?: { + dangerousEntityRefFallback?: { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + }; + }, ): Promise; resolveOwnershipEntityRefs(entity: Entity): Promise<{ ownershipEntityRefs: string[]; @@ -146,12 +157,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..2b7442f746 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 + ? { entityRef: { 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 + ? { entityRef: { name: localPart } } + : undefined, + }, + ); }; }, }); diff --git a/plugins/auth-node/src/types.ts b/plugins/auth-node/src/types.ts index a7202d7748..8d6b8f34fb 100644 --- a/plugins/auth-node/src/types.ts +++ b/plugins/auth-node/src/types.ts @@ -162,10 +162,26 @@ 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?: { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + }; + }, ): Promise; /** diff --git a/yarn.lock b/yarn.lock index 91c2126f7c..036bb86974 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5275,6 +5275,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 @@ -5314,6 +5315,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 @@ -5332,6 +5334,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 @@ -5350,6 +5353,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 @@ -5368,6 +5372,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 @@ -5389,6 +5394,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 @@ -5404,6 +5410,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 @@ -5421,6 +5428,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 @@ -5439,6 +5447,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 @@ -5456,6 +5465,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 @@ -5493,6 +5503,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 @@ -5510,6 +5521,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 @@ -5523,6 +5535,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" jose: "npm:^5.0.0" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -5547,6 +5560,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 @@ -5565,6 +5579,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 @@ -5583,6 +5598,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