Merge pull request #32133 from JessicaJHee/sync-gl-id
feat(catalog): add gitlab user ID in user entity
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-github-provider': minor
|
||||
---
|
||||
|
||||
Added the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by their GitHub user ID.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-gitlab-provider': minor
|
||||
'@backstage/plugin-catalog-backend-module-gitlab': minor
|
||||
---
|
||||
|
||||
Added the `{gitlab-integration-host}/user-id` annotation to store GitLab's user ID (immutable) in user entities. Also includes addition of the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by the new ID.
|
||||
@@ -80,6 +80,7 @@ This provider includes several resolvers out of the box that you can use:
|
||||
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found, it will throw a `NotFoundError`.
|
||||
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`.
|
||||
- `usernameMatchingUserEntityName`: Matches the username from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`.
|
||||
- `userIdMatchingUserEntityAnnotation`: Matches the GitHub user ID with the User entity that has a matching `github.com/user-id`. If no match is found, it will throw a `NotFoundError`.
|
||||
|
||||
:::note Note
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ This provider includes several resolvers out of the box that you can use:
|
||||
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found, it will throw a `NotFoundError`.
|
||||
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`.
|
||||
- `usernameMatchingUserEntityName`: Matches the username from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`.
|
||||
- `userIdMatchingUserEntityAnnotation`: Matches the GitLab user ID with the User entity that has a matching `gitlab.com/user-id` annotation (or `{integration-host}/user-id` for self-hosted GitLab instances). If no match is found, it will throw a `NotFoundError`.
|
||||
|
||||
:::note Note
|
||||
|
||||
|
||||
@@ -251,6 +251,46 @@ browser when viewing that user.
|
||||
This annotation can be used on a [User entity](descriptor-format.md#kind-user)
|
||||
to note that it originated from that user on GitHub.
|
||||
|
||||
### github.com/user-id
|
||||
|
||||
```yaml
|
||||
# Example:
|
||||
metadata:
|
||||
annotations:
|
||||
github.com/user-id: '123456'
|
||||
```
|
||||
|
||||
The value of this annotation is the numeric user ID that identifies a user on
|
||||
[GitHub](https://github.com) (either the public one, or a private GitHub
|
||||
Enterprise installation) that is related to this entity. Unlike the username,
|
||||
which can be changed by the user, the user ID is immutable.
|
||||
|
||||
This annotation can be used on a [User entity](descriptor-format.md#kind-user)
|
||||
to note that it originated from that user on GitHub. It enables the
|
||||
`userIdMatchingUserEntityAnnotation` sign-in resolver to match users by their
|
||||
GitHub user ID during authentication.
|
||||
|
||||
### gitlab.com/user-id
|
||||
|
||||
```yaml
|
||||
# Example:
|
||||
metadata:
|
||||
annotations:
|
||||
gitlab.com/user-id: '123456'
|
||||
```
|
||||
|
||||
The value of this annotation is the numeric user ID that identifies a user on
|
||||
[GitLab](https://gitlab.com) (either the public one, or a private GitLab
|
||||
installation) that is related to this entity. For self-hosted GitLab instances,
|
||||
the annotation key will be `{integration-host}/user-id` where
|
||||
`{integration-host}` is the hostname of your GitLab instance. Unlike the
|
||||
username, which can be changed, the user ID is immutable.
|
||||
|
||||
This annotation can be used on a [User entity](descriptor-format.md#kind-user)
|
||||
to note that it originated from that user on GitLab. It enables the
|
||||
`userIdMatchingUserEntityAnnotation` sign-in resolver to match users by their
|
||||
GitLab user ID during authentication.
|
||||
|
||||
### gocd.org/pipelines
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -44,6 +44,10 @@ export interface Config {
|
||||
resolver: 'preferredUsernameMatchingUserEntityName';
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
|
||||
}
|
||||
| {
|
||||
resolver: 'userIdMatchingUserEntityAnnotation';
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
|
||||
}
|
||||
>;
|
||||
};
|
||||
sessionDuration?: HumanDuration | string;
|
||||
|
||||
@@ -45,6 +45,10 @@ export interface Config {
|
||||
resolver: 'emailMatchingUserEntityProfileEmail';
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
|
||||
}
|
||||
| {
|
||||
resolver: 'userIdMatchingUserEntityAnnotation';
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
|
||||
}
|
||||
>;
|
||||
};
|
||||
sessionDuration?: HumanDuration | string;
|
||||
|
||||
@@ -17,13 +17,26 @@ export default authModuleGitlabProvider;
|
||||
// @public (undocumented)
|
||||
export const gitlabAuthenticator: OAuthAuthenticator<
|
||||
PassportOAuthAuthenticatorHelper,
|
||||
PassportProfile
|
||||
GitlabProfile
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type GitlabProfile = PassportProfile & {
|
||||
id?: string;
|
||||
profileUrl?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export namespace gitlabSignInResolvers {
|
||||
const usernameMatchingUserEntityName: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
OAuthAuthenticatorResult<GitlabProfile>,
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
const userIdMatchingUserEntityAnnotation: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<GitlabProfile>,
|
||||
| {
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,16 @@ import {
|
||||
} from '@backstage/plugin-auth-node';
|
||||
|
||||
/** @public */
|
||||
export const gitlabAuthenticator = createOAuthAuthenticator({
|
||||
export type GitlabProfile = PassportProfile & {
|
||||
id?: string;
|
||||
profileUrl?: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const gitlabAuthenticator = createOAuthAuthenticator<
|
||||
PassportOAuthAuthenticatorHelper,
|
||||
GitlabProfile
|
||||
>({
|
||||
defaultProfileTransform:
|
||||
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
|
||||
scopes: {
|
||||
@@ -55,7 +64,7 @@ export const gitlabAuthenticator = createOAuthAuthenticator({
|
||||
) => {
|
||||
done(
|
||||
undefined,
|
||||
{ fullProfile, params, accessToken },
|
||||
{ fullProfile: fullProfile as GitlabProfile, params, accessToken },
|
||||
{ refreshToken },
|
||||
);
|
||||
},
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { gitlabAuthenticator } from './authenticator';
|
||||
export { gitlabAuthenticator, type GitlabProfile } from './authenticator';
|
||||
export { authModuleGitlabProvider as default } from './module';
|
||||
export { gitlabSignInResolvers } from './resolvers';
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
import {
|
||||
createSignInResolverFactory,
|
||||
OAuthAuthenticatorResult,
|
||||
PassportProfile,
|
||||
SignInInfo,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { GitlabProfile } from './authenticator';
|
||||
|
||||
/**
|
||||
* Available sign-in resolvers for the GitLab auth provider.
|
||||
*
|
||||
@@ -39,7 +40,7 @@ export namespace gitlabSignInResolvers {
|
||||
.optional(),
|
||||
create(options = {}) {
|
||||
return async (
|
||||
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
info: SignInInfo<OAuthAuthenticatorResult<GitlabProfile>>,
|
||||
ctx,
|
||||
) => {
|
||||
const { result } = info;
|
||||
@@ -63,4 +64,51 @@ export namespace gitlabSignInResolvers {
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Looks up the user by matching their GitLab user ID to the user-id annotation.
|
||||
*/
|
||||
export const userIdMatchingUserEntityAnnotation = createSignInResolverFactory(
|
||||
{
|
||||
optionsSchema: z
|
||||
.object({
|
||||
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
create(options = {}) {
|
||||
return async (
|
||||
info: SignInInfo<OAuthAuthenticatorResult<GitlabProfile>>,
|
||||
ctx,
|
||||
) => {
|
||||
const { fullProfile } = info.result;
|
||||
|
||||
const userId = fullProfile.id;
|
||||
if (!userId) {
|
||||
throw new Error(`GitLab user profile does not contain a user ID`);
|
||||
}
|
||||
|
||||
if (!fullProfile.profileUrl) {
|
||||
throw new Error(
|
||||
`GitLab user profile does not contain a profile URL`,
|
||||
);
|
||||
}
|
||||
const host = new URL(fullProfile.profileUrl).hostname;
|
||||
|
||||
return ctx.signInWithCatalogUser(
|
||||
{
|
||||
annotations: {
|
||||
[`${host}/user-id`]: userId,
|
||||
},
|
||||
},
|
||||
{
|
||||
dangerousEntityRefFallback:
|
||||
options?.dangerouslyAllowSignInWithoutUserInCatalog
|
||||
? { entityRef: { name: userId } }
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2191,6 +2191,7 @@ export const expected_single_user_entity: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://example.com/JohnDoe',
|
||||
'example.com/user-login': 'https://gitlab.example/john_doe',
|
||||
'example.com/user-id': '1',
|
||||
},
|
||||
name: 'JohnDoe',
|
||||
},
|
||||
@@ -2218,6 +2219,7 @@ export const expected_single_user_removed_entity: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://example.com/johndoe',
|
||||
'example.com/user-login': '',
|
||||
'example.com/user-id': '1',
|
||||
},
|
||||
name: 'johndoe',
|
||||
},
|
||||
@@ -2245,6 +2247,7 @@ export const expected_full_org_scan_entities: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://example.com/JohnDoe',
|
||||
'example.com/user-login': 'https://gitlab.example/john_doe',
|
||||
'example.com/user-id': '1',
|
||||
},
|
||||
name: 'JohnDoe',
|
||||
},
|
||||
@@ -2269,6 +2272,7 @@ export const expected_full_org_scan_entities: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://example.com/JaneDoe',
|
||||
'example.com/user-login': 'https://gitlab.example/jane_doe',
|
||||
'example.com/user-id': '2',
|
||||
},
|
||||
name: 'JaneDoe',
|
||||
},
|
||||
@@ -2294,6 +2298,7 @@ export const expected_full_org_scan_entities: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://example.com/MarySmith',
|
||||
'example.com/user-login': 'https://gitlab.example/mary_smith',
|
||||
'example.com/user-id': '3',
|
||||
},
|
||||
name: 'MarySmith',
|
||||
},
|
||||
@@ -2319,6 +2324,7 @@ export const expected_full_org_scan_entities: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://example.com/MarioMario',
|
||||
'example.com/user-login': 'https://gitlab.example/mario_mario',
|
||||
'example.com/user-id': '5',
|
||||
},
|
||||
name: 'MarioMario',
|
||||
},
|
||||
@@ -2395,6 +2401,7 @@ export const expected_full_org_scan_entities_saas: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/testuser1',
|
||||
'gitlab.com/user-login': 'https://gitlab.com/testuser1',
|
||||
'gitlab.com/user-id': '12',
|
||||
'gitlab.com/saml-external-uid': '51',
|
||||
},
|
||||
name: 'testuser1',
|
||||
@@ -2421,6 +2428,7 @@ export const expected_full_org_scan_entities_saas: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/testuser2',
|
||||
'gitlab.com/user-login': 'https://gitlab.com/testuser2',
|
||||
'gitlab.com/user-id': '34',
|
||||
'gitlab.com/saml-external-uid': '52',
|
||||
},
|
||||
name: 'testuser2',
|
||||
@@ -2447,6 +2455,7 @@ export const expected_full_org_scan_entities_saas: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/testuser4',
|
||||
'gitlab.com/user-login': 'https://gitlab.com/testuser4',
|
||||
'gitlab.com/user-id': '44',
|
||||
'gitlab.com/saml-external-uid': '54',
|
||||
},
|
||||
name: 'testuser4',
|
||||
@@ -2473,6 +2482,7 @@ export const expected_full_org_scan_entities_saas: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/testuser3',
|
||||
'gitlab.com/user-login': 'https://gitlab.com/testuser3',
|
||||
'gitlab.com/user-id': '33',
|
||||
'gitlab.com/saml-external-uid': '53',
|
||||
},
|
||||
name: 'testuser3',
|
||||
@@ -2503,6 +2513,7 @@ export const expected_full_org_scan_entities_includeUsersWithoutSeat_saas: MockO
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/testuser1',
|
||||
'gitlab.com/user-login': 'https://gitlab.com/testuser1',
|
||||
'gitlab.com/user-id': '12',
|
||||
'gitlab.com/saml-external-uid': '51',
|
||||
},
|
||||
name: 'testuser1',
|
||||
@@ -2529,6 +2540,7 @@ export const expected_full_org_scan_entities_includeUsersWithoutSeat_saas: MockO
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/testuser2',
|
||||
'gitlab.com/user-login': 'https://gitlab.com/testuser2',
|
||||
'gitlab.com/user-id': '34',
|
||||
'gitlab.com/saml-external-uid': '52',
|
||||
},
|
||||
name: 'testuser2',
|
||||
@@ -2555,6 +2567,7 @@ export const expected_full_org_scan_entities_includeUsersWithoutSeat_saas: MockO
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/testusernoseat1',
|
||||
'gitlab.com/user-login': 'https://gitlab.com/testusernoseat1',
|
||||
'gitlab.com/user-id': '36',
|
||||
'gitlab.com/saml-external-uid': '60',
|
||||
},
|
||||
name: 'testusernoseat1',
|
||||
@@ -2581,6 +2594,7 @@ export const expected_full_org_scan_entities_includeUsersWithoutSeat_saas: MockO
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/testuser4',
|
||||
'gitlab.com/user-login': 'https://gitlab.com/testuser4',
|
||||
'gitlab.com/user-id': '44',
|
||||
'gitlab.com/saml-external-uid': '54',
|
||||
},
|
||||
name: 'testuser4',
|
||||
@@ -2607,6 +2621,7 @@ export const expected_full_org_scan_entities_includeUsersWithoutSeat_saas: MockO
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/testuser3',
|
||||
'gitlab.com/user-login': 'https://gitlab.com/testuser3',
|
||||
'gitlab.com/user-id': '33',
|
||||
'gitlab.com/saml-external-uid': '53',
|
||||
},
|
||||
name: 'testuser3',
|
||||
@@ -2673,6 +2688,7 @@ export const expected_subgroup_org_scan_entities_saas: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/testuser1',
|
||||
'gitlab.com/user-login': 'https://gitlab.com/testuser1',
|
||||
'gitlab.com/user-id': '12',
|
||||
'gitlab.com/saml-external-uid': '51',
|
||||
},
|
||||
name: 'testuser1',
|
||||
@@ -2702,6 +2718,7 @@ export const expected_full_members_group_org_scan_entities: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://example.com/JohnDoe',
|
||||
'example.com/user-login': 'https://gitlab.example/john_doe',
|
||||
'example.com/user-id': '1',
|
||||
},
|
||||
name: 'JohnDoe',
|
||||
},
|
||||
@@ -2726,6 +2743,7 @@ export const expected_full_members_group_org_scan_entities: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://example.com/JaneDoe',
|
||||
'example.com/user-login': 'https://gitlab.example/jane_doe',
|
||||
'example.com/user-id': '2',
|
||||
},
|
||||
name: 'JaneDoe',
|
||||
},
|
||||
@@ -2751,6 +2769,7 @@ export const expected_full_members_group_org_scan_entities: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://example.com/MarySmith',
|
||||
'example.com/user-login': 'https://gitlab.example/mary_smith',
|
||||
'example.com/user-id': '3',
|
||||
},
|
||||
name: 'MarySmith',
|
||||
},
|
||||
@@ -2776,6 +2795,7 @@ export const expected_full_members_group_org_scan_entities: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://example.com/MarioMario',
|
||||
'example.com/user-login': 'https://gitlab.example/mario_mario',
|
||||
'example.com/user-id': '5',
|
||||
},
|
||||
name: 'MarioMario',
|
||||
},
|
||||
@@ -2851,6 +2871,7 @@ export const expected_group_members_group_org_scan_entities: MockObject[] = [
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://example.com/JohnDoe',
|
||||
'example.com/user-login': 'https://gitlab.example/john_doe',
|
||||
'example.com/user-id': '1',
|
||||
},
|
||||
name: 'JohnDoe',
|
||||
},
|
||||
|
||||
@@ -103,6 +103,8 @@ export function defaultUserTransformer(
|
||||
|
||||
annotations[`${options.integrationConfig.host}/user-login`] =
|
||||
options.user.web_url;
|
||||
annotations[`${options.integrationConfig.host}/user-id`] =
|
||||
options.user.id.toString();
|
||||
if (options.user?.group_saml_identity?.extern_uid) {
|
||||
annotations[`${options.integrationConfig.host}/saml-external-uid`] =
|
||||
options.user.group_saml_identity.extern_uid;
|
||||
|
||||
Reference in New Issue
Block a user