From ea9262bc9ff443f1f3791567e05e33f0f8d0c345 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Tue, 6 Feb 2024 15:21:22 +0200 Subject: [PATCH] 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 --- .changeset/selfish-pigs-glow.md | 6 +++ plugins/auth-backend/api-report.md | 12 ++++-- plugins/auth-backend/src/index.ts | 2 +- .../resolvers/CatalogAuthResolverContext.ts | 30 ++++--------- .../resolvers/DefaultAuthOwnershipResolver.ts | 43 +++++++++++++++++++ .../auth-backend/src/lib/resolvers/index.ts | 6 +-- plugins/auth-backend/src/providers/router.ts | 11 ++++- plugins/auth-backend/src/service/router.ts | 8 ++-- plugins/auth-node/api-report.md | 6 +++ plugins/auth-node/src/index.ts | 1 + plugins/auth-node/src/types.ts | 9 ++++ 11 files changed, 99 insertions(+), 35 deletions(-) create mode 100644 .changeset/selfish-pigs-glow.md create mode 100644 plugins/auth-backend/src/lib/resolvers/DefaultAuthOwnershipResolver.ts 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.