Merge pull request #28967 from JessicaJHee/dangerouslyAllowSignInWithoutUserInCatalog-config
introduce dangerouslyAllowSignInWithoutUserInCatalog auth resolver config
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Added support for the new `dangerousEntityRefFallback` option for `signInWithCatalogUser` in `AuthResolverContext`.
|
||||
@@ -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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -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.
|
||||
@@ -304,6 +304,7 @@ oidc
|
||||
Okta
|
||||
Olausson
|
||||
Oldsberg
|
||||
onboarded
|
||||
onboarding
|
||||
Onboarding
|
||||
onelogin
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -20,7 +20,10 @@ export const atlassianAuthenticator: OAuthAuthenticator<
|
||||
export namespace atlassianSignInResolvers {
|
||||
const usernameMatchingUserEntityName: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -46,8 +46,12 @@ export interface Config {
|
||||
| {
|
||||
resolver: 'emailLocalPartMatchingUserEntityName';
|
||||
allowedDomains?: string[];
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
|
||||
}
|
||||
| {
|
||||
resolver: 'emailMatchingUserEntityProfileEmail';
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
|
||||
}
|
||||
| { resolver: 'emailMatchingUserEntityProfileEmail' }
|
||||
>;
|
||||
};
|
||||
sessionDuration?: HumanDuration | string;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -41,7 +41,10 @@ export namespace awsAlbSignInResolvers {
|
||||
const // (undocumented)
|
||||
emailMatchingUserEntityProfileEmail: SignInResolverFactory<
|
||||
AwsAlbResult,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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<AwsAlbResult>, 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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -32,7 +32,10 @@ export namespace azureEasyAuthSignInResolvers {
|
||||
const // (undocumented)
|
||||
idMatchingUserEntityAnnotation: SignInResolverFactory<
|
||||
AzureEasyAuthResult,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AzureEasyAuthResult>, 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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
+13
-2
@@ -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;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -24,11 +24,17 @@ export const bitbucketAuthenticator: OAuthAuthenticator<
|
||||
export namespace bitbucketSignInResolvers {
|
||||
const userIdMatchingUserEntityAnnotation: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
const usernameMatchingUserEntityAnnotation: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -27,7 +27,10 @@ export const bitbucketServerAuthenticator: OAuthAuthenticator<
|
||||
export namespace bitbucketServerSignInResolvers {
|
||||
const emailMatchingUserEntityProfileEmail: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,8 +34,12 @@ export interface Config {
|
||||
| {
|
||||
resolver: 'emailLocalPartMatchingUserEntityName';
|
||||
allowedDomains?: string[];
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
|
||||
}
|
||||
| {
|
||||
resolver: 'emailMatchingUserEntityProfileEmail';
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
|
||||
}
|
||||
| { resolver: 'emailMatchingUserEntityProfileEmail' }
|
||||
>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -52,7 +52,10 @@ export type CloudflareAccessResult = {
|
||||
export namespace cloudflareAccessSignInResolvers {
|
||||
const emailMatchingUserEntityProfileEmail: SignInResolverFactory<
|
||||
CloudflareAccessResult,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,13 @@ describe('resolvers', () => {
|
||||
} satisfies Partial<AuthResolverContext>;
|
||||
|
||||
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,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<CloudflareAccessResult>, 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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
+13
-3
@@ -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;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<GcpIapResult>, 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<GcpIapResult>, 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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
+10
-4
@@ -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;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -24,7 +24,10 @@ export const githubAuthenticator: OAuthAuthenticator<
|
||||
export namespace githubSignInResolvers {
|
||||
const usernameMatchingUserEntityName: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
+9
-2
@@ -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;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -24,7 +24,10 @@ export const gitlabAuthenticator: OAuthAuthenticator<
|
||||
export namespace gitlabSignInResolvers {
|
||||
const usernameMatchingUserEntityName: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
+9
-2
@@ -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;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -24,7 +24,10 @@ export const googleAuthenticator: OAuthAuthenticator<
|
||||
export namespace googleSignInResolvers {
|
||||
const emailMatchingUserEntityAnnotation: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
+13
-3
@@ -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;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -30,11 +30,17 @@ export const microsoftAuthenticator: OAuthAuthenticator<
|
||||
export namespace microsoftSignInResolvers {
|
||||
const emailMatchingUserEntityAnnotation: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
const userIdMatchingUserEntityAnnotation: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
+9
-2
@@ -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;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -24,7 +24,10 @@ export const oauth2Authenticator: OAuthAuthenticator<
|
||||
export namespace oauth2SignInResolvers {
|
||||
const usernameMatchingUserEntityName: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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<JWTPayload = {}> = {
|
||||
headers: IncomingHttpHeaders;
|
||||
getHeader(name: string): string | undefined;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export namespace oauth2ProxySignInResolvers {
|
||||
const // (undocumented)
|
||||
forwardedUserMatchingUserEntityName: SignInResolverFactory<
|
||||
OAuth2ProxyResult,
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export { authModuleOauth2ProxyProvider as default } from './module';
|
||||
export { oauth2ProxySignInResolvers } from './resolvers';
|
||||
export {
|
||||
oauth2ProxyAuthenticator,
|
||||
OAUTH2_PROXY_JWT_HEADER,
|
||||
|
||||
@@ -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<OAuth2ProxyResult>, 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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
+9
-1
@@ -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;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
+9
-2
@@ -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;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -24,7 +24,10 @@ export const oktaAuthenticator: OAuthAuthenticator<
|
||||
export namespace oktaSignInResolvers {
|
||||
const emailMatchingUserEntityAnnotation: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -24,7 +24,10 @@ export const oneLoginAuthenticator: OAuthAuthenticator<
|
||||
export namespace oneLoginSignInResolvers {
|
||||
const usernameMatchingUserEntityName: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,8 +32,12 @@ export interface Config {
|
||||
| {
|
||||
resolver: 'emailLocalPartMatchingUserEntityName';
|
||||
allowedDomains?: string[];
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
|
||||
}
|
||||
| {
|
||||
resolver: 'emailMatchingUserEntityProfileEmail';
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
|
||||
}
|
||||
| { resolver: 'emailMatchingUserEntityProfileEmail' }
|
||||
>;
|
||||
};
|
||||
sessionDuration?: HumanDuration | string;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -110,6 +110,17 @@ export type AuthResolverContext = {
|
||||
}>;
|
||||
signInWithCatalogUser(
|
||||
query: AuthResolverCatalogUserQuery,
|
||||
options?: {
|
||||
dangerousEntityRefFallback?: {
|
||||
entityRef:
|
||||
| string
|
||||
| {
|
||||
kind?: string;
|
||||
namespace?: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
},
|
||||
): Promise<BackstageSignInResult>;
|
||||
resolveOwnershipEntityRefs(entity: Entity): Promise<{
|
||||
ownershipEntityRefs: string[];
|
||||
@@ -146,12 +157,17 @@ export type ClientAuthResponse<TProviderInfo> = {
|
||||
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
|
||||
>;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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<BackstageSignInResult>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user