Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support configuration to include transient group ownership during auth #2210

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,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 ...
```
2 changes: 1 addition & 1 deletion docs/dynamic-plugins/export-derived-package.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ If you are developing your own plugin that is going to be used as a dynamic plug
To be compatible with the showcase dynamic plugin support, and used as dynamic plugins, existing plugins must be based on, or compatible with, the new backend system, as well as rebuilt with a dedicated CLI command.

The new backend system standard entry point (created using `createBackendPlugin()` or `createBackendModule()`) should be exported as the default export of either the main package or of an `alpha` package (if the new backend support is still provided as `alpha` APIs). This doesn't add any additional requirement on top of the standard plugin development guidelines of the new backend system.
For a practical example of a dynamic plugin entry point built upon the new backend system, please refer to the [Janus plugins repository](https://github.com/janus-idp/backstage-plugins/blob/main/plugins/aap-backend/src/module.ts#L25).
For a practical example of a dynamic plugin entry point built upon the new backend system, please refer to the [Janus plugins repository](https://github.com/backstage/community-plugins/blob/main/workspaces/3scale/plugins/3scale-backend/src/module.ts).

The dynamic export mechanism identifies private, non-backstage dependencies, and sets the `bundleDependencies` field in the `package.json` file for them, so that the dynamic plugin package can be published as a self-contained package, along with its private dependencies bundled in a private `node_modules` folder.

Expand Down
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 @@ -65,7 +66,9 @@
"winston": "3.14.2"
},
"devDependencies": {
"@backstage/backend-test-utils": "1.2.1",
"@backstage/cli": "0.29.6",
"@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' });
});
});
21 changes: 19 additions & 2 deletions packages/backend/src/modules/authProvidersModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ 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';
import { rhdhSignInResolvers } from './authResolvers';

/**
Expand All @@ -36,7 +38,7 @@ import { rhdhSignInResolvers } from './authResolvers';
* @param ctx
* @returns
*/
async function signInWithCatalogUserOptional(
export async function signInWithCatalogUserOptional(
name: string | AuthResolverCatalogUserQuery,
ctx: AuthResolverContext,
) {
Expand Down Expand Up @@ -289,9 +291,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 @@ -310,6 +322,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
Loading