auth-backend: add helper methods in AuthResolverContext + deprecations
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -9,7 +9,9 @@ import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
|
||||
import { BackstageSignInResult } from '@backstage/plugin-auth-node';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import express from 'express';
|
||||
import { GetEntitiesRequest } from '@backstage/catalog-client';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { Logger } from 'winston';
|
||||
import { PluginDatabaseManager } from '@backstage/backend-common';
|
||||
@@ -110,11 +112,40 @@ export interface AuthProviderRouteHandlers {
|
||||
start(req: express.Request, res: express.Response): Promise<void>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "AuthResolverCatalogUserQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type AuthResolverCatalogUserQuery =
|
||||
| {
|
||||
entityRef:
|
||||
| string
|
||||
| {
|
||||
kind?: string;
|
||||
namespace?: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
annotations: Record<string, string>;
|
||||
}
|
||||
| {
|
||||
filter: Exclude<GetEntitiesRequest['filter'], undefined>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type AuthResolverContext = {
|
||||
logger: Logger;
|
||||
tokenIssuer: TokenIssuer;
|
||||
catalogIdentityClient: CatalogIdentityClient;
|
||||
logger: Logger;
|
||||
issueToken(params: TokenParams): Promise<{
|
||||
token: string;
|
||||
}>;
|
||||
findCatalogUser(query: AuthResolverCatalogUserQuery): Promise<{
|
||||
entity: Entity;
|
||||
}>;
|
||||
signInWithCatalogUser(
|
||||
query: AuthResolverCatalogUserQuery,
|
||||
): Promise<BackstageSignInResult>;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "AuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
@@ -360,10 +391,12 @@ export type GcpIapTokenInfo = {
|
||||
[key: string]: JsonValue;
|
||||
};
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts
|
||||
// @public
|
||||
export function getDefaultOwnershipEntityRefs(entity: Entity): string[];
|
||||
|
||||
// Warning: (ae-missing-release-tag) "getEntityClaims" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export function getEntityClaims(entity: UserEntity): TokenParams['claims'];
|
||||
|
||||
// Warning: (ae-missing-release-tag) "GithubOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
@@ -760,5 +793,6 @@ export type WebMessageResponse =
|
||||
// src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/github/provider.d.ts:97:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/types.d.ts:131:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/types.d.ts:50:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/types.d.ts:180:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
|
||||
```
|
||||
|
||||
@@ -32,3 +32,5 @@ export * from './lib/flow';
|
||||
export * from './lib/oauth';
|
||||
|
||||
export * from './lib/catalog';
|
||||
|
||||
export { getDefaultOwnershipEntityRefs } from './lib/resolvers';
|
||||
|
||||
@@ -21,6 +21,9 @@ import {
|
||||
} from '@backstage/catalog-model';
|
||||
import { TokenParams } from '../../identity';
|
||||
|
||||
/**
|
||||
* @deprecated use {@link getDefaultOwnershipEntityRefs} instead
|
||||
*/
|
||||
export function getEntityClaims(entity: UserEntity): TokenParams['claims'] {
|
||||
const userRef = stringifyEntityRef(entity);
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2022 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 { TokenManager } from '@backstage/backend-common';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import {
|
||||
DEFAULT_NAMESPACE,
|
||||
Entity,
|
||||
parseEntityRef,
|
||||
RELATION_MEMBER_OF,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { ConflictError, InputError, NotFoundError } from '@backstage/errors';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../..';
|
||||
import { TokenParams } from '../../identity';
|
||||
import { AuthResolverContext } from '../../providers';
|
||||
import { AuthResolverCatalogUserQuery } from '../../providers/types';
|
||||
import { CatalogIdentityClient } from '../catalog';
|
||||
|
||||
/**
|
||||
* 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)
|
||||
.map(r => r.targetRef) ?? [];
|
||||
|
||||
return Array.from(new Set([stringifyEntityRef(entity), ...membershipRefs]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CatalogAuthResolverContext implements AuthResolverContext {
|
||||
static create(options: {
|
||||
logger: Logger;
|
||||
catalogApi: CatalogApi;
|
||||
tokenIssuer: TokenIssuer;
|
||||
tokenManager: TokenManager;
|
||||
}): CatalogAuthResolverContext {
|
||||
const catalogIdentityClient = new CatalogIdentityClient({
|
||||
catalogApi: options.catalogApi,
|
||||
tokenManager: options.tokenManager,
|
||||
});
|
||||
return new CatalogAuthResolverContext(
|
||||
options.logger,
|
||||
options.tokenIssuer,
|
||||
catalogIdentityClient,
|
||||
options.catalogApi,
|
||||
options.tokenManager,
|
||||
);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
public readonly logger: Logger,
|
||||
public readonly tokenIssuer: TokenIssuer,
|
||||
public readonly catalogIdentityClient: CatalogIdentityClient,
|
||||
private readonly catalogApi: CatalogApi,
|
||||
private readonly tokenManager: TokenManager,
|
||||
) {}
|
||||
|
||||
async issueToken(params: TokenParams) {
|
||||
const token = await this.tokenIssuer.issueToken(params);
|
||||
return { token };
|
||||
}
|
||||
|
||||
async findCatalogUser(query: AuthResolverCatalogUserQuery) {
|
||||
let result: Entity[] | Entity | undefined = undefined;
|
||||
const { token } = await this.tokenManager.getToken();
|
||||
|
||||
if ('entityRef' in query) {
|
||||
const entityRef = parseEntityRef(query.entityRef, {
|
||||
defaultKind: 'user',
|
||||
defaultNamespace: DEFAULT_NAMESPACE,
|
||||
});
|
||||
result = await this.catalogApi.getEntityByRef(entityRef, { token });
|
||||
} else if ('annotations' in query) {
|
||||
const filter: Record<string, string> = {
|
||||
kind: 'user',
|
||||
};
|
||||
for (const [key, value] of Object.entries(query.annotations)) {
|
||||
filter[`metadata.annotations.${key}`] = value;
|
||||
}
|
||||
const res = await this.catalogApi.getEntities({ filter }, { token });
|
||||
result = res.items;
|
||||
} else if ('filter' in query) {
|
||||
const res = await this.catalogApi.getEntities(
|
||||
{ filter: query.filter },
|
||||
{ token },
|
||||
);
|
||||
result = res.items;
|
||||
} else {
|
||||
throw new InputError('Invalid user lookup query');
|
||||
}
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
if (result.length > 1) {
|
||||
throw new ConflictError('User lookup resulted in multiple matches');
|
||||
}
|
||||
result = result[0];
|
||||
}
|
||||
if (!result) {
|
||||
throw new NotFoundError('User not found');
|
||||
}
|
||||
|
||||
return { entity: result };
|
||||
}
|
||||
|
||||
async signInWithCatalogUser(query: AuthResolverCatalogUserQuery) {
|
||||
const { entity } = await this.findCatalogUser(query);
|
||||
const ownershipRefs = getDefaultOwnershipEntityRefs(entity);
|
||||
|
||||
const token = await this.tokenIssuer.issueToken({
|
||||
claims: {
|
||||
sub: stringifyEntityRef(entity),
|
||||
ent: ownershipRefs,
|
||||
},
|
||||
});
|
||||
return { token };
|
||||
}
|
||||
/*
|
||||
async expandCatalogOwnership(query: { entityRefs: string[] }) {
|
||||
const { entityRefs } = query;
|
||||
|
||||
const compoundRefs = entityRefs.map(ref =>
|
||||
parseEntityRef(ref.toLocaleLowerCase('en-US'), {
|
||||
defaultKind: 'user',
|
||||
defaultNamespace: DEFAULT_NAMESPACE,
|
||||
}),
|
||||
);
|
||||
const stringRefs = compoundRefs.map(e => stringifyEntityRef(e));
|
||||
|
||||
const { token } = await this.tokenManager.getToken();
|
||||
const { items: entities } = await this.catalogApi.getEntities(
|
||||
{
|
||||
filter: compoundRefs.map(ref => ({
|
||||
kind: ref.kind,
|
||||
'metadata.namespace': ref.namespace,
|
||||
'metadata.name': ref.name,
|
||||
})),
|
||||
},
|
||||
{ token },
|
||||
);
|
||||
|
||||
if (compoundRefs.length !== entities.length) {
|
||||
const found = entities.map(e => stringifyEntityRef(e));
|
||||
const missing = stringRefs.filter(ref => !found.includes(ref));
|
||||
throw new NotFoundError(`Entities not found for refs ${missing.join()}`);
|
||||
}
|
||||
|
||||
const memberOf = entities.flatMap(e => getDefaultOwnershipEntityRefs(e));
|
||||
|
||||
return Array.from(new Set(memberOf));
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
CatalogAuthResolverContext,
|
||||
getDefaultOwnershipEntityRefs,
|
||||
} from './CatalogAuthResolverContext';
|
||||
@@ -17,9 +17,7 @@
|
||||
import { GoogleAuthProvider } from './provider';
|
||||
import * as helpers from '../../lib/passport/PassportStrategyHelper';
|
||||
import { OAuthResult } from '../../lib/oauth';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { TokenIssuer } from '../../identity/types';
|
||||
import { CatalogIdentityClient } from '../../lib/catalog';
|
||||
import { AuthResolverContext } from '../types';
|
||||
|
||||
const mockFrameHandler = jest.spyOn(
|
||||
helpers,
|
||||
@@ -30,21 +28,8 @@ const mockFrameHandler = jest.spyOn(
|
||||
|
||||
describe('createGoogleProvider', () => {
|
||||
it('should auth', async () => {
|
||||
const tokenIssuer = {
|
||||
issueToken: jest.fn(),
|
||||
listPublicKeys: jest.fn(),
|
||||
};
|
||||
const catalogIdentityClient = {
|
||||
findUser: jest.fn(),
|
||||
};
|
||||
|
||||
const provider = new GoogleAuthProvider({
|
||||
resolverContext: {
|
||||
logger: getVoidLogger(),
|
||||
catalogIdentityClient:
|
||||
catalogIdentityClient as unknown as CatalogIdentityClient,
|
||||
tokenIssuer: tokenIssuer as unknown as TokenIssuer,
|
||||
},
|
||||
resolverContext: {} as AuthResolverContext,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: {
|
||||
email: fullProfile.emails![0]!.value,
|
||||
|
||||
@@ -41,6 +41,7 @@ export type {
|
||||
AuthProviderFactoryOptions,
|
||||
AuthProviderFactory,
|
||||
AuthHandler,
|
||||
AuthResolverCatalogUserQuery,
|
||||
AuthResolverContext,
|
||||
AuthHandlerResult,
|
||||
SignInResolver,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
PluginEndpointDiscovery,
|
||||
TokenManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { CatalogApi, GetEntitiesRequest } from '@backstage/catalog-client';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
BackstageIdentityResponse,
|
||||
@@ -26,9 +26,40 @@ import {
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../identity/types';
|
||||
import { TokenIssuer, TokenParams } from '../identity/types';
|
||||
import { OAuthStartRequest } from '../lib/oauth/types';
|
||||
import { CatalogIdentityClient } from '../lib/catalog';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
/**
|
||||
* A query for a single user in the catalog.
|
||||
*
|
||||
* If `entityRef` is used, the default kind is `'User'`.
|
||||
*
|
||||
* If `annotations` are used, all annotations must be present and
|
||||
* match the provided value exactly. Only entities of kind `'User'` will be considered.
|
||||
*
|
||||
* If `filter` are used they are passed on as they are to the `CatalogApi`.
|
||||
*
|
||||
* Regardless of the query method, the query must match exactly one entity
|
||||
* in the catalog, or an error will be thrown.
|
||||
*/
|
||||
export type AuthResolverCatalogUserQuery =
|
||||
| {
|
||||
entityRef:
|
||||
| string
|
||||
| {
|
||||
kind?: string;
|
||||
namespace?: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
annotations: Record<string, string>;
|
||||
}
|
||||
| {
|
||||
filter: Exclude<GetEntitiesRequest['filter'], undefined>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The context that is used for auth processing.
|
||||
@@ -36,9 +67,36 @@ import { CatalogIdentityClient } from '../lib/catalog';
|
||||
* @public
|
||||
*/
|
||||
export type AuthResolverContext = {
|
||||
tokenIssuer: TokenIssuer;
|
||||
catalogIdentityClient: CatalogIdentityClient;
|
||||
/** @deprecated Will be removed from the context, access it via a closure instead if needed */
|
||||
logger: Logger;
|
||||
/** @deprecated Use the `issueToken` method instead */
|
||||
tokenIssuer: TokenIssuer;
|
||||
/** @deprecated Use the `findCatalogUser` and `signInWithCatalogUser` methods instead, and the `getDefaultOwnershipEntityRefs` helper */
|
||||
catalogIdentityClient: CatalogIdentityClient;
|
||||
|
||||
/**
|
||||
* Issues a Backstage token using the provided parameters.
|
||||
*/
|
||||
issueToken(params: TokenParams): Promise<{ token: string }>;
|
||||
|
||||
/**
|
||||
* Finds a single user in the catalog using the provided query.
|
||||
*
|
||||
* See {@link AuthResolverCatalogUserQuery} for details.
|
||||
*/
|
||||
findCatalogUser(
|
||||
query: AuthResolverCatalogUserQuery,
|
||||
): Promise<{ entity: Entity }>;
|
||||
|
||||
/**
|
||||
* Finds a single user in the catalog using the provided query, and then
|
||||
* issues an identity for that user using default ownership resolution.
|
||||
*
|
||||
* See {@link AuthResolverCatalogUserQuery} for details.
|
||||
*/
|
||||
signInWithCatalogUser(
|
||||
query: AuthResolverCatalogUserQuery,
|
||||
): Promise<BackstageSignInResult>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,7 +34,7 @@ import { createOidcRouter, TokenFactory, KeyStores } from '../identity';
|
||||
import session from 'express-session';
|
||||
import passport from 'passport';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { CatalogIdentityClient } from '../lib/catalog';
|
||||
import { CatalogAuthResolverContext } from '../lib/resolvers';
|
||||
|
||||
type ProviderFactories = { [s: string]: AuthProviderFactory };
|
||||
|
||||
@@ -104,11 +104,6 @@ export async function createRouter(
|
||||
|
||||
const isOriginAllowed = createOriginFilter(config);
|
||||
|
||||
const catalogIdentityClient = new CatalogIdentityClient({
|
||||
catalogApi,
|
||||
tokenManager,
|
||||
});
|
||||
|
||||
for (const [providerId, providerFactory] of Object.entries(
|
||||
allProviderFactories,
|
||||
)) {
|
||||
@@ -128,11 +123,12 @@ export async function createRouter(
|
||||
tokenIssuer,
|
||||
discovery,
|
||||
catalogApi,
|
||||
resolverContext: {
|
||||
resolverContext: CatalogAuthResolverContext.create({
|
||||
logger,
|
||||
catalogApi,
|
||||
tokenIssuer,
|
||||
catalogIdentityClient,
|
||||
},
|
||||
tokenManager,
|
||||
}),
|
||||
});
|
||||
|
||||
const r = Router();
|
||||
|
||||
Reference in New Issue
Block a user