diff --git a/.changeset/selfish-pigs-glow.md b/.changeset/selfish-pigs-glow.md new file mode 100644 index 0000000000..ca29a22feb --- /dev/null +++ b/.changeset/selfish-pigs-glow.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-auth-node': patch +--- + +Allow overriding default ownership resolving diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 1beddb6419..0f537d3dc2 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -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; +// @public +export class DefaultAuthOwnershipResolver implements AuthOwnershipResolver { + // (undocumented) + getOwnershipEntityRefs(entity: Entity): Promise; +} + // @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; diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index 6c3f866031..d3688934c5 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -34,4 +34,4 @@ export * from './lib/oauth'; export * from './lib/catalog'; -export { getDefaultOwnershipEntityRefs } from './lib/resolvers'; +export { DefaultAuthOwnershipResolver } from './lib/resolvers'; diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts index 4f9673ac79..be0cd0da97 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -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: { diff --git a/plugins/auth-backend/src/lib/resolvers/DefaultAuthOwnershipResolver.ts b/plugins/auth-backend/src/lib/resolvers/DefaultAuthOwnershipResolver.ts new file mode 100644 index 0000000000..3b6bab861f --- /dev/null +++ b/plugins/auth-backend/src/lib/resolvers/DefaultAuthOwnershipResolver.ts @@ -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 { + 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])); + } +} diff --git a/plugins/auth-backend/src/lib/resolvers/index.ts b/plugins/auth-backend/src/lib/resolvers/index.ts index c1ca59cb25..a9c132b386 100644 --- a/plugins/auth-backend/src/lib/resolvers/index.ts +++ b/plugins/auth-backend/src/lib/resolvers/index.ts @@ -14,7 +14,5 @@ * limitations under the License. */ -export { - CatalogAuthResolverContext, - getDefaultOwnershipEntityRefs, -} from './CatalogAuthResolverContext'; +export { CatalogAuthResolverContext } from './CatalogAuthResolverContext'; +export { DefaultAuthOwnershipResolver } from './DefaultAuthOwnershipResolver'; diff --git a/plugins/auth-backend/src/providers/router.ts b/plugins/auth-backend/src/providers/router.ts index 8c927f52c6..d2000288a7 100644 --- a/plugins/auth-backend/src/providers/router.ts +++ b/plugins/auth-backend/src/providers/router.ts @@ -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, }), }); diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index f1487d07a2..11c150aa33 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -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 */ diff --git a/plugins/auth-node/api-report.md b/plugins/auth-node/api-report.md index e79facc7f9..da3166d3f2 100644 --- a/plugins/auth-node/api-report.md +++ b/plugins/auth-node/api-report.md @@ -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; +} + // @public @deprecated (undocumented) export type AuthProviderConfig = { baseUrl: string; diff --git a/plugins/auth-node/src/index.ts b/plugins/auth-node/src/index.ts index 8b35aaa1e1..39063bf7c2 100644 --- a/plugins/auth-node/src/index.ts +++ b/plugins/auth-node/src/index.ts @@ -43,5 +43,6 @@ export type { SignInInfo, SignInResolver, TokenParams, + AuthOwnershipResolver, } from './types'; export { tokenTypes } from './types'; diff --git a/plugins/auth-node/src/types.ts b/plugins/auth-node/src/types.ts index 39172437ec..4dc5e570de 100644 --- a/plugins/auth-node/src/types.ts +++ b/plugins/auth-node/src/types.ts @@ -163,6 +163,15 @@ export type AuthResolverContext = { ): Promise; }; +/** + * Resolver interface for resolving the ownership entity references for entity + * + * @public + */ +export interface AuthOwnershipResolver { + getOwnershipEntityRefs(entity: Entity): Promise; +} + /** * 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.