Skip to content

Commit

Permalink
Support configuration to include transient group ownership during auth
Browse files Browse the repository at this point in the history
Signed-off-by: Jessica He <[email protected]>
  • Loading branch information
JessicaJHee committed Feb 3, 2025
1 parent 3510574 commit 0281d34
Show file tree
Hide file tree
Showing 8 changed files with 620 additions and 3 deletions.
24 changes: 24 additions & 0 deletions docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,27 @@ auth:
providers:
# provider configs ...
```

### includeTransientGroupOwnership configuration value

This option allows users to add transient parent groups into the resolved user group membership during the authentication process. i.e., the parent group of the user's direct group will be included in the user ownership entities. By default, this option is set to false.

For instance, with this group hierarchy:

```
group_admin
└── group_developers
└── user_alice
```

- If `includeTransientGroupOwnership: false`, `test_user` is only a member of `group_developers`.
- If `includeTransientGroupOwnership: true`, `test_user` is a member of `group_developers` AND `group_admin`.

To enable this option:

```yaml
includeTransientGroupOwnership: true
auth:
providers:
# provider configs ...
```
5 changes: 5 additions & 0 deletions packages/app/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,9 @@ export interface Config {
* @visibility frontend
*/
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
/**
* The option to includes transient parent groups when determining user group membership
* @visibility frontend
*/
includeTransientGroupOwnership?: boolean;
}
3 changes: 3 additions & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@backstage/backend-defaults": "0.7.0",
"@backstage/backend-dynamic-feature-service": "0.5.3",
"@backstage/backend-plugin-api": "1.1.1",
"@backstage/catalog-client": "1.9.1",
"@backstage/catalog-model": "1.7.3",
"@backstage/cli-node": "0.2.12",
"@backstage/config": "1.3.2",
Expand Down Expand Up @@ -64,7 +65,9 @@
"winston": "3.14.2"
},
"devDependencies": {
"@backstage/backend-test-utils": "1.2.1",
"@backstage/cli": "0.29.5",
"@backstage/plugin-catalog-node": "1.15.1",
"@types/express": "4.17.21",
"@types/global-agent": "2.1.3",
"prettier": "3.4.2"
Expand Down
75 changes: 75 additions & 0 deletions packages/backend/src/modules/authProvidersModule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { AuthResolverContext } from '@backstage/plugin-auth-node';

import { signInWithCatalogUserOptional } from './authProvidersModule';

jest.mock('@backstage/config-loader', () => ({
ConfigSources: {
default: jest.fn(),
toConfig: jest.fn().mockResolvedValue({
getOptionalBoolean: jest.fn().mockReturnValue(false), // mock dangerouslyAllowSignInWithoutUserInCatalog to false by default
}),
},
}));

describe('signInWithCatalogUserOptional', () => {
let ctx: jest.Mocked<AuthResolverContext>;

beforeEach(() => {
jest.clearAllMocks();
ctx = {
issueToken: jest.fn().mockResolvedValue({
token: 'issuedBackstageUserToken',
}),
findCatalogUser: jest.fn(),
signInWithCatalogUser: jest.fn().mockResolvedValue({
token: 'backstageToken',
}),
};
});

it('should return the signedInUser with token if the user exists in the catalog', async () => {
const signedInUser = await signInWithCatalogUserOptional('test-user', ctx);

expect(ctx.signInWithCatalogUser).toHaveBeenCalledWith({
entityRef: { name: 'test-user' },
});

expect(signedInUser).toEqual({ token: 'backstageToken' });
});

it('should issue a token if the user does not exist in catalog and dangerouslyAllowSignInWithoutUserInCatalog is false (by default)', async () => {
(ctx.signInWithCatalogUser as jest.Mock).mockRejectedValue(
new Error('User not found'),
);

await expect(
signInWithCatalogUserOptional('test-user', ctx),
).rejects.toThrow(
`Sign in failed: User not found in the RHDH software catalog. Verify that users/groups are synchronized to the software catalog. For non-production environments, manually provision the user or disable the user provisioning requirement. Refer to the RHDH Authentication documentation for further details.`,
);

expect(ctx.signInWithCatalogUser).toHaveBeenCalledWith({
entityRef: { name: 'test-user' },
});
});

it('should issue a token if the user does not exist in catalog and dangerouslyAllowSignInWithoutUserInCatalog is true', async () => {
const { ConfigSources } = require('@backstage/config-loader');
ConfigSources.toConfig.mockResolvedValue({
getOptionalBoolean: jest.fn().mockReturnValue(true),
});
(ctx.signInWithCatalogUser as jest.Mock).mockRejectedValue(
new Error('User not found'),
);

const result = await signInWithCatalogUserOptional('test-user', ctx);
expect(ctx.issueToken).toHaveBeenCalledWith({
claims: {
sub: 'user:default/test-user',
ent: ['user:default/test-user'],
},
});

expect(result).toEqual({ token: 'issuedBackstageUserToken' });
});
});
22 changes: 20 additions & 2 deletions packages/backend/src/modules/authProvidersModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ import {
oidcSignInResolvers,
} from '@backstage/plugin-auth-backend-module-oidc-provider';
import {
authOwnershipResolutionExtensionPoint,
AuthProviderFactory,
authProvidersExtensionPoint,
AuthResolverCatalogUserQuery,
AuthResolverContext,
createOAuthProviderFactory,
} from '@backstage/plugin-auth-node';

import { TransientGroupOwnershipResolver } from '../transientGroupOwnershipResolver';

/**
* Function is responsible for signing in a user with the catalog user and
* creating an entity reference based on the provided name parameter.
Expand All @@ -34,7 +37,7 @@ import {
* @param ctx
* @returns
*/
async function signInWithCatalogUserOptional(
export async function signInWithCatalogUserOptional(
name: string | AuthResolverCatalogUserQuery,
ctx: AuthResolverContext,
) {
Expand Down Expand Up @@ -283,9 +286,19 @@ const authProvidersModule = createBackendModule({
deps: {
config: coreServices.rootConfig,
authProviders: authProvidersExtensionPoint,
authOwnershipResolution: authOwnershipResolutionExtensionPoint,
logger: coreServices.logger,
discovery: coreServices.discovery,
auth: coreServices.auth
},
async init({ config, authProviders, logger }) {
async init({
config,
authProviders,
authOwnershipResolution,
logger,
discovery,
auth
}) {
const providersConfig = config.getConfig('auth.providers');
const authFactories: ProviderFactories = {};
providersConfig
Expand All @@ -304,6 +317,11 @@ const authProvidersModule = createBackendModule({
logger.info(
`Enabled Provider Factories : ${JSON.stringify(providerFactories)}`,
);
const transientGroupOwnershipResolver =
new TransientGroupOwnershipResolver({ discovery, config, auth });
authOwnershipResolution.setAuthOwnershipResolver(
transientGroupOwnershipResolver,
);

Object.entries(providerFactories).forEach(([providerId, factory]) => {
authProviders.registerProvider({ providerId, factory });
Expand Down
Loading

0 comments on commit 0281d34

Please sign in to comment.