feat: allow overriding default ownership resolving

This allows to modify the ownership resolving in the auth resolve
context. For example if user wants to include parent groups also to
the ownershipEntityRefs, it's not possible unless the built-in
auth providers are forked and rewritten.

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2024-02-06 15:21:22 +02:00
parent 16ef9e59e5
commit ea9262bc9f
11 changed files with 99 additions and 35 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-auth-backend': patch
'@backstage/plugin-auth-node': patch
---
Allow overriding default ownership resolving
+9 -3
View File
@@ -3,6 +3,7 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AuthOwnershipResolver } from '@backstage/plugin-auth-node';
import { AuthProviderConfig as AuthProviderConfig_2 } from '@backstage/plugin-auth-node';
import { AuthProviderFactory as AuthProviderFactory_2 } from '@backstage/plugin-auth-node';
import { AuthProviderRouteHandlers as AuthProviderRouteHandlers_2 } from '@backstage/plugin-auth-node';
@@ -193,6 +194,12 @@ export function createOriginFilter(config: Config): (origin: string) => boolean;
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public
export class DefaultAuthOwnershipResolver implements AuthOwnershipResolver {
// (undocumented)
getOwnershipEntityRefs(entity: Entity): Promise<string[]>;
}
// @public
export const defaultAuthProviderFactories: {
[providerId: string]: AuthProviderFactory_2;
@@ -216,9 +223,6 @@ export type GcpIapResult = GcpIapResult_2;
// @public @deprecated
export type GcpIapTokenInfo = GcpIapTokenInfo_2;
// @public
export function getDefaultOwnershipEntityRefs(entity: Entity): string[];
// @public (undocumented)
export type GithubOAuthResult = {
fullProfile: Profile;
@@ -668,6 +672,8 @@ export interface RouterOptions {
// (undocumented)
logger: LoggerService;
// (undocumented)
ownershipResolver?: AuthOwnershipResolver;
// (undocumented)
providerFactories?: ProviderFactories;
// (undocumented)
tokenFactoryAlgorithm?: string;
+1 -1
View File
@@ -34,4 +34,4 @@ export * from './lib/oauth';
export * from './lib/catalog';
export { getDefaultOwnershipEntityRefs } from './lib/resolvers';
export { DefaultAuthOwnershipResolver } from './lib/resolvers';
@@ -20,7 +20,6 @@ import {
DEFAULT_NAMESPACE,
Entity,
parseEntityRef,
RELATION_MEMBER_OF,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { ConflictError, InputError, NotFoundError } from '@backstage/errors';
@@ -31,31 +30,13 @@ import {
LoggerService,
} from '@backstage/backend-plugin-api';
import { TokenIssuer } from '../../identity/types';
import { CatalogIdentityClient } from '../catalog';
import {
AuthOwnershipResolver,
AuthResolverCatalogUserQuery,
AuthResolverContext,
TokenParams,
} from '@backstage/plugin-auth-node';
/**
* Uses the default ownership resolution logic to return an array
* of entity refs that the provided entity claims ownership through.
*
* A reference to the entity itself will also be included in the returned array.
*
* @public
*/
export function getDefaultOwnershipEntityRefs(entity: Entity) {
const membershipRefs =
entity.relations
?.filter(
r => r.type === RELATION_MEMBER_OF && r.targetRef.startsWith('group:'),
)
.map(r => r.targetRef) ?? [];
return Array.from(new Set([stringifyEntityRef(entity), ...membershipRefs]));
}
import { CatalogIdentityClient } from '../catalog';
/**
* @internal
@@ -69,6 +50,7 @@ export class CatalogAuthResolverContext implements AuthResolverContext {
discovery: DiscoveryService;
auth: AuthService;
httpAuth: HttpAuthService;
ownershipResolver: AuthOwnershipResolver;
}): CatalogAuthResolverContext {
const catalogIdentityClient = new CatalogIdentityClient({
catalogApi: options.catalogApi,
@@ -84,6 +66,7 @@ export class CatalogAuthResolverContext implements AuthResolverContext {
catalogIdentityClient,
options.catalogApi,
options.auth,
options.ownershipResolver,
);
}
@@ -93,6 +76,7 @@ export class CatalogAuthResolverContext implements AuthResolverContext {
public readonly catalogIdentityClient: CatalogIdentityClient,
private readonly catalogApi: CatalogApi,
private readonly auth: AuthService,
private readonly ownershipResolver: AuthOwnershipResolver,
) {}
async issueToken(params: TokenParams) {
@@ -160,7 +144,9 @@ export class CatalogAuthResolverContext implements AuthResolverContext {
async signInWithCatalogUser(query: AuthResolverCatalogUserQuery) {
const { entity } = await this.findCatalogUser(query);
const ownershipRefs = getDefaultOwnershipEntityRefs(entity);
const ownershipRefs = await this.ownershipResolver.getOwnershipEntityRefs(
entity,
);
const token = await this.tokenIssuer.issueToken({
claims: {
@@ -0,0 +1,43 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AuthOwnershipResolver } from '@backstage/plugin-auth-node';
import {
Entity,
RELATION_MEMBER_OF,
stringifyEntityRef,
} from '@backstage/catalog-model';
/**
* Uses the default ownership resolution logic to return an array
* of entity refs that the provided entity claims ownership through.
*
* A reference to the entity itself will also be included in the returned array.
*
* @public
*/
export class DefaultAuthOwnershipResolver implements AuthOwnershipResolver {
async getOwnershipEntityRefs(entity: Entity): Promise<string[]> {
const membershipRefs =
entity.relations
?.filter(
r =>
r.type === RELATION_MEMBER_OF && r.targetRef.startsWith('group:'),
)
.map(r => r.targetRef) ?? [];
return Array.from(new Set([stringifyEntityRef(entity), ...membershipRefs]));
}
}
@@ -14,7 +14,5 @@
* limitations under the License.
*/
export {
CatalogAuthResolverContext,
getDefaultOwnershipEntityRefs,
} from './CatalogAuthResolverContext';
export { CatalogAuthResolverContext } from './CatalogAuthResolverContext';
export { DefaultAuthOwnershipResolver } from './DefaultAuthOwnershipResolver';
+9 -2
View File
@@ -25,13 +25,17 @@ import {
} from '@backstage/backend-plugin-api';
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { NotFoundError, assertError } from '@backstage/errors';
import { AuthProviderFactory } from '@backstage/plugin-auth-node';
import { assertError, NotFoundError } from '@backstage/errors';
import {
AuthOwnershipResolver,
AuthProviderFactory,
} from '@backstage/plugin-auth-node';
import express from 'express';
import Router from 'express-promise-router';
import { Minimatch } from 'minimatch';
import { CatalogAuthResolverContext } from '../lib/resolvers/CatalogAuthResolverContext';
import { TokenIssuer } from '../identity/types';
import { DefaultAuthOwnershipResolver } from '../lib/resolvers';
/** @public */
export type ProviderFactories = { [s: string]: AuthProviderFactory };
@@ -49,6 +53,7 @@ export function bindProviderRouters(
httpAuth: HttpAuthService;
tokenManager: TokenManager;
tokenIssuer: TokenIssuer;
ownershipResolver?: AuthOwnershipResolver;
catalogApi?: CatalogApi;
},
) {
@@ -64,6 +69,7 @@ export function bindProviderRouters(
tokenManager,
tokenIssuer,
catalogApi,
ownershipResolver = new DefaultAuthOwnershipResolver(),
} = options;
const providersConfig = config.getOptionalConfig('auth.providers');
@@ -95,6 +101,7 @@ export function bindProviderRouters(
discovery,
auth,
httpAuth,
ownershipResolver,
}),
});
+5 -3
View File
@@ -23,15 +23,16 @@ import {
LoggerService,
} from '@backstage/backend-plugin-api';
import { defaultAuthProviderFactories } from '../providers';
import { AuthOwnershipResolver } from '@backstage/plugin-auth-node';
import {
createLegacyAuthAdapters,
PluginDatabaseManager,
PluginEndpointDiscovery,
TokenManager,
createLegacyAuthAdapters,
} from '@backstage/backend-common';
import { NotFoundError } from '@backstage/errors';
import { CatalogApi } from '@backstage/catalog-client';
import { bindOidcRouter, TokenFactory, KeyStores } from '../identity';
import { bindOidcRouter, KeyStores, TokenFactory } from '../identity';
import session from 'express-session';
import connectSessionKnex from 'connect-session-knex';
import passport from 'passport';
@@ -41,7 +42,7 @@ import { TokenIssuer } from '../identity/types';
import { StaticTokenIssuer } from '../identity/StaticTokenIssuer';
import { StaticKeyStore } from '../identity/StaticKeyStore';
import { Config } from '@backstage/config';
import { ProviderFactories, bindProviderRouters } from '../providers/router';
import { bindProviderRouters, ProviderFactories } from '../providers/router';
/** @public */
export interface RouterOptions {
@@ -56,6 +57,7 @@ export interface RouterOptions {
providerFactories?: ProviderFactories;
disableDefaultProviderFactories?: boolean;
catalogApi?: CatalogApi;
ownershipResolver?: AuthOwnershipResolver;
}
/** @public */
+6
View File
@@ -21,6 +21,12 @@ import { Strategy } from 'passport';
import { ZodSchema } from 'zod';
import { ZodTypeDef } from 'zod';
// @public
export interface AuthOwnershipResolver {
// (undocumented)
getOwnershipEntityRefs(entity: Entity): Promise<string[]>;
}
// @public @deprecated (undocumented)
export type AuthProviderConfig = {
baseUrl: string;
+1
View File
@@ -43,5 +43,6 @@ export type {
SignInInfo,
SignInResolver,
TokenParams,
AuthOwnershipResolver,
} from './types';
export { tokenTypes } from './types';
+9
View File
@@ -163,6 +163,15 @@ export type AuthResolverContext = {
): Promise<BackstageSignInResult>;
};
/**
* Resolver interface for resolving the ownership entity references for entity
*
* @public
*/
export interface AuthOwnershipResolver {
getOwnershipEntityRefs(entity: Entity): Promise<string[]>;
}
/**
* Any Auth provider needs to implement this interface which handles the routes in the
* auth backend. Any auth API requests from the frontend reaches these methods.