backend-{plugin,app}-api: refactored auth APIs to use principals
Co-authored-by: Fredrik Adelöw <freben@gmail.com> Co-authored-by: Carl-Erik Bergström <cbergstrom@spotify.com> Co-authored-by: blam <ben@blam.sh> Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
+28
-15
@@ -19,12 +19,15 @@ import {
|
||||
mockServices,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import {
|
||||
InternalBackstageServiceCredentials,
|
||||
InternalBackstageUserCredentials,
|
||||
InternalBackstageCredentials,
|
||||
authServiceFactory,
|
||||
} from './authServiceFactory';
|
||||
import { decodeJwt } from 'jose';
|
||||
import { discoveryServiceFactory } from '../discovery';
|
||||
import {
|
||||
BackstageServicePrincipal,
|
||||
BackstageUserPrincipal,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
// TODO: Ship discovery mock service in the service factory tester
|
||||
const mockDeps = [
|
||||
@@ -48,18 +51,22 @@ describe('authServiceFactory', () => {
|
||||
const searchAuth = await tester.get('search');
|
||||
const catalogAuth = await tester.get('catalog');
|
||||
|
||||
const { token: searchToken } = await searchAuth.issueServiceToken();
|
||||
const { token: searchToken } = await searchAuth.issueToken({});
|
||||
|
||||
await expect(searchAuth.authenticate(searchToken)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'service',
|
||||
subject: 'external:backstage-plugin',
|
||||
principal: {
|
||||
type: 'service',
|
||||
subject: 'external:backstage-plugin',
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(catalogAuth.authenticate(searchToken)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'service',
|
||||
subject: 'external:backstage-plugin',
|
||||
principal: {
|
||||
type: 'service',
|
||||
subject: 'external:backstage-plugin',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -72,14 +79,17 @@ describe('authServiceFactory', () => {
|
||||
const catalogAuth = await tester.get('catalog');
|
||||
|
||||
await expect(
|
||||
catalogAuth.issueServiceToken({
|
||||
catalogAuth.issueToken({
|
||||
forward: {
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
version: 'v1',
|
||||
type: 'user',
|
||||
userEntityRef: 'user:default/alice',
|
||||
authMethod: 'token',
|
||||
token: 'alice-token',
|
||||
} as InternalBackstageUserCredentials,
|
||||
principal: {
|
||||
type: 'user',
|
||||
userEntityRef: 'user:default/alice',
|
||||
},
|
||||
} as InternalBackstageCredentials<BackstageUserPrincipal>,
|
||||
}),
|
||||
).resolves.toEqual({ token: 'alice-token' });
|
||||
});
|
||||
@@ -91,14 +101,17 @@ describe('authServiceFactory', () => {
|
||||
|
||||
const catalogAuth = await tester.get('catalog');
|
||||
|
||||
const { token } = await catalogAuth.issueServiceToken({
|
||||
const { token } = await catalogAuth.issueToken({
|
||||
forward: {
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
version: 'v1',
|
||||
type: 'service',
|
||||
subject: 'external:backstage-plugin',
|
||||
authMethod: 'token',
|
||||
token: 'some-upstream-service-token',
|
||||
} as InternalBackstageServiceCredentials,
|
||||
principal: {
|
||||
type: 'service',
|
||||
subject: 'external:upstream-service',
|
||||
},
|
||||
} as InternalBackstageCredentials<BackstageServicePrincipal>,
|
||||
});
|
||||
|
||||
expect(decodeJwt(token)).toEqual(
|
||||
|
||||
@@ -18,8 +18,10 @@ import { ServerTokenManager, TokenManager } from '@backstage/backend-common';
|
||||
import {
|
||||
AuthService,
|
||||
BackstageCredentials,
|
||||
BackstageServiceCredentials,
|
||||
BackstageUserCredentials,
|
||||
BackstagePrincipalTypes,
|
||||
BackstageServicePrincipal,
|
||||
BackstageNonePrincipal,
|
||||
BackstageUserPrincipal,
|
||||
IdentityService,
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
@@ -32,58 +34,76 @@ import {
|
||||
import { decodeJwt } from 'jose';
|
||||
|
||||
/** @internal */
|
||||
export type InternalBackstageServiceCredentials =
|
||||
BackstageServiceCredentials & {
|
||||
export type InternalBackstageCredentials<TPrincipal = unknown> =
|
||||
BackstageCredentials<TPrincipal> & {
|
||||
version: string;
|
||||
token: string;
|
||||
token?: string;
|
||||
authMethod: 'token' | 'cookie' | 'none';
|
||||
};
|
||||
|
||||
function createServiceCredentials(
|
||||
export function createCredentialsWithServicePrincipal(
|
||||
sub: string,
|
||||
token: string,
|
||||
): BackstageServiceCredentials {
|
||||
): InternalBackstageCredentials<BackstageServicePrincipal> {
|
||||
return {
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
version: 'v1',
|
||||
token,
|
||||
type: 'service',
|
||||
subject: sub,
|
||||
} as InternalBackstageServiceCredentials;
|
||||
principal: {
|
||||
type: 'service',
|
||||
subject: sub,
|
||||
},
|
||||
authMethod: 'token',
|
||||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type InternalBackstageUserCredentials = BackstageUserCredentials & {
|
||||
version: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
function createUserCredentials(
|
||||
export function createCredentialsWithUserPrincipal(
|
||||
sub: string,
|
||||
token: string,
|
||||
): BackstageUserCredentials {
|
||||
authMethod: 'token' | 'cookie' = 'token',
|
||||
): InternalBackstageCredentials<BackstageUserPrincipal> {
|
||||
return {
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
version: 'v1',
|
||||
token,
|
||||
type: 'user',
|
||||
userEntityRef: sub,
|
||||
} as InternalBackstageUserCredentials;
|
||||
principal: {
|
||||
type: 'user',
|
||||
userEntityRef: sub,
|
||||
},
|
||||
authMethod,
|
||||
};
|
||||
}
|
||||
|
||||
export function createCredentialsWithNonePrincipal(): InternalBackstageCredentials<BackstageNonePrincipal> {
|
||||
return {
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
version: 'v1',
|
||||
principal: {
|
||||
type: 'none',
|
||||
},
|
||||
authMethod: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
export function toInternalBackstageCredentials(
|
||||
credentials: BackstageCredentials,
|
||||
): InternalBackstageServiceCredentials | InternalBackstageUserCredentials {
|
||||
): InternalBackstageCredentials<
|
||||
BackstageUserPrincipal | BackstageServicePrincipal | BackstageNonePrincipal
|
||||
> {
|
||||
if (credentials.$$type !== '@backstage/BackstageCredentials') {
|
||||
throw new Error('Invalid credential type');
|
||||
}
|
||||
const internalCredentials = credentials as
|
||||
| InternalBackstageServiceCredentials
|
||||
| InternalBackstageUserCredentials;
|
||||
|
||||
const internalCredentials = credentials as InternalBackstageCredentials<
|
||||
BackstageUserPrincipal | BackstageServicePrincipal | BackstageNonePrincipal
|
||||
>;
|
||||
|
||||
if (internalCredentials.version !== 'v1') {
|
||||
throw new Error(
|
||||
`Invalid credential version ${internalCredentials.version}`,
|
||||
);
|
||||
}
|
||||
|
||||
return internalCredentials;
|
||||
}
|
||||
|
||||
@@ -100,7 +120,10 @@ class DefaultAuthService implements AuthService {
|
||||
// Legacy service-to-service token
|
||||
if (sub === 'backstage-server' && !aud) {
|
||||
await this.tokenManager.authenticate(token);
|
||||
return createServiceCredentials('external:backstage-plugin', token);
|
||||
return createCredentialsWithServicePrincipal(
|
||||
'external:backstage-plugin',
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
// User Backstage token
|
||||
@@ -111,21 +134,42 @@ class DefaultAuthService implements AuthService {
|
||||
} as IdentityApiGetIdentityRequest);
|
||||
|
||||
if (!identity) {
|
||||
throw new AuthenticationError('No identity found');
|
||||
throw new AuthenticationError('Invalid user token');
|
||||
}
|
||||
|
||||
return createUserCredentials(identity.identity.userEntityRef, token);
|
||||
return createCredentialsWithUserPrincipal(
|
||||
identity.identity.userEntityRef,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
async issueServiceToken(options?: {
|
||||
isPrincipal<TType extends keyof BackstagePrincipalTypes>(
|
||||
credentials: BackstageCredentials,
|
||||
type: TType,
|
||||
): credentials is BackstageCredentials<BackstagePrincipalTypes[TType]> {
|
||||
const principal = credentials.principal as
|
||||
| BackstageUserPrincipal
|
||||
| BackstageServicePrincipal;
|
||||
|
||||
if (principal.type !== type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async issueToken(options?: {
|
||||
forward?: BackstageCredentials;
|
||||
}): Promise<{ token: string }> {
|
||||
const internalForward =
|
||||
options?.forward && toInternalBackstageCredentials(options.forward);
|
||||
|
||||
if (internalForward) {
|
||||
const { type } = internalForward;
|
||||
const { type } = internalForward.principal;
|
||||
if (type === 'user') {
|
||||
if (!internalForward.token) {
|
||||
throw new Error('User credentials is unexpectedly missing token');
|
||||
}
|
||||
return { token: internalForward.token };
|
||||
} else if (type !== 'service') {
|
||||
throw new AuthenticationError(
|
||||
|
||||
+72
-49
@@ -16,19 +16,21 @@
|
||||
|
||||
import {
|
||||
AuthService,
|
||||
BackstageCredentialTypes,
|
||||
BackstageCredentials,
|
||||
BackstageUnauthorizedCredentials,
|
||||
BackstageHttpAccessToPrincipalTypesMapping,
|
||||
DiscoveryService,
|
||||
HttpAuthService,
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { NotAllowedError } from '@backstage/errors';
|
||||
import { AuthenticationError, NotAllowedError } from '@backstage/errors';
|
||||
import { parse as parseCookie } from 'cookie';
|
||||
import { Request, Response } from 'express';
|
||||
import { decodeJwt } from 'jose';
|
||||
import { toInternalBackstageCredentials } from '../auth/authServiceFactory';
|
||||
import {
|
||||
createCredentialsWithNonePrincipal,
|
||||
toInternalBackstageCredentials,
|
||||
} from '../auth/authServiceFactory';
|
||||
|
||||
const BACKSTAGE_AUTH_COOKIE = 'backstage-auth';
|
||||
|
||||
@@ -58,20 +60,9 @@ function getTokenFromRequest(req: Request) {
|
||||
const credentialsSymbol = Symbol('backstage-credentials');
|
||||
|
||||
type RequestWithCredentials = Request & {
|
||||
[credentialsSymbol]?: Promise<{
|
||||
credentials: BackstageCredentials | BackstageUnauthorizedCredentials;
|
||||
// TODO: This is temporary and should be removed once we have proper cookie handling in place
|
||||
isCookie: boolean;
|
||||
}>;
|
||||
[credentialsSymbol]?: Promise<BackstageCredentials>;
|
||||
};
|
||||
|
||||
function createUnauthorizedCredentials(): BackstageUnauthorizedCredentials {
|
||||
return {
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
type: 'unauthorized',
|
||||
};
|
||||
}
|
||||
|
||||
class DefaultHttpAuthService implements HttpAuthService {
|
||||
constructor(
|
||||
private readonly auth: AuthService,
|
||||
@@ -79,63 +70,94 @@ class DefaultHttpAuthService implements HttpAuthService {
|
||||
private readonly pluginId: string,
|
||||
) {}
|
||||
|
||||
async #getCredentials(req: Request) {
|
||||
async #extractCredentialsFromRequest(req: Request) {
|
||||
const { token, isCookie } = getTokenFromRequest(req);
|
||||
// TODO: Is this where we match against configured rules?
|
||||
if (!token) {
|
||||
return { credentials: createUnauthorizedCredentials(), isCookie };
|
||||
return createCredentialsWithNonePrincipal();
|
||||
}
|
||||
return { credentials: await this.auth.authenticate(token), isCookie };
|
||||
}
|
||||
|
||||
async #getCredentialsCached(req: /* */ RequestWithCredentials) {
|
||||
return (req[credentialsSymbol] ??= this.#getCredentials(req));
|
||||
}
|
||||
|
||||
async credentials<TAllowed extends keyof BackstageCredentialTypes>(
|
||||
req: RequestWithCredentials,
|
||||
options: {
|
||||
allow: TAllowed[];
|
||||
},
|
||||
): Promise<BackstageCredentialTypes[TAllowed]> {
|
||||
const { credentials, isCookie } = await this.#getCredentialsCached(req);
|
||||
|
||||
if (credentials.type === 'user' && isCookie) {
|
||||
if (options.allow.includes('user-cookie' as TAllowed)) {
|
||||
return credentials as BackstageCredentialTypes[TAllowed];
|
||||
const credentials = toInternalBackstageCredentials(
|
||||
await this.auth.authenticate(token),
|
||||
);
|
||||
if (isCookie) {
|
||||
if (credentials.principal.type !== 'user') {
|
||||
throw new AuthenticationError(
|
||||
'Refusing to authenticate non-user principal with cookie auth',
|
||||
);
|
||||
}
|
||||
throw new NotAllowedError(
|
||||
`This endpoint does not allow 'user-cookie' credentials`,
|
||||
);
|
||||
credentials.authMethod = 'cookie';
|
||||
}
|
||||
|
||||
if (!options.allow.includes(credentials.type as TAllowed)) {
|
||||
throw new NotAllowedError(
|
||||
`This endpoint does not allow '${credentials.type}' credentials`,
|
||||
);
|
||||
}
|
||||
|
||||
return credentials as BackstageCredentialTypes[TAllowed];
|
||||
return credentials;
|
||||
}
|
||||
|
||||
async requestHeaders(options?: {
|
||||
async #getCredentials(req: /* */ RequestWithCredentials) {
|
||||
return (req[credentialsSymbol] ??=
|
||||
this.#extractCredentialsFromRequest(req));
|
||||
}
|
||||
|
||||
async credentials<
|
||||
TAllowed extends
|
||||
| keyof BackstageHttpAccessToPrincipalTypesMapping
|
||||
| undefined = undefined,
|
||||
>(
|
||||
req: Request,
|
||||
options?: {
|
||||
allow: Array<TAllowed>;
|
||||
},
|
||||
): Promise<
|
||||
BackstageCredentials<
|
||||
TAllowed extends keyof BackstageHttpAccessToPrincipalTypesMapping
|
||||
? BackstageHttpAccessToPrincipalTypesMapping[TAllowed]
|
||||
: unknown
|
||||
>
|
||||
> {
|
||||
const credentials = toInternalBackstageCredentials(
|
||||
await this.#getCredentials(req),
|
||||
);
|
||||
|
||||
// TODO break out into more readable function as we always
|
||||
// want to check cookie regardless if options are set or not
|
||||
// Probably asserts function that ensures authMethod = 'token' after cookie check
|
||||
if (credentials.authMethod === 'cookie') {
|
||||
if (options && !options.allow.includes('user-cookie' as TAllowed)) {
|
||||
throw new NotAllowedError(
|
||||
`This endpoint does not allow 'user-cookie' credentials`,
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
options &&
|
||||
!options.allow.includes(credentials.principal.type as TAllowed)
|
||||
) {
|
||||
throw new NotAllowedError(
|
||||
`This endpoint does not allow '${credentials.principal.type}' credentials`,
|
||||
);
|
||||
}
|
||||
|
||||
return credentials as any;
|
||||
}
|
||||
|
||||
async requestHeaders(options: {
|
||||
forward?: BackstageCredentials;
|
||||
}): Promise<Record<string, string>> {
|
||||
return {
|
||||
Authorization: `Bearer ${await this.auth.issueServiceToken(options)}`,
|
||||
Authorization: `Bearer ${await this.auth.issueToken(options)}`,
|
||||
};
|
||||
}
|
||||
|
||||
async issueUserCookie(res: Response): Promise<void> {
|
||||
const credentials = await this.credentials(res.req, { allow: ['user'] });
|
||||
|
||||
// https://backstage.spotify.net/api/catalog
|
||||
// https://backstage.example.com/api/catalog
|
||||
const externalBaseUrlStr = await this.discovery.getExternalBaseUrl(
|
||||
this.pluginId,
|
||||
);
|
||||
const externalBaseUrl = new URL(externalBaseUrlStr);
|
||||
|
||||
const { token } = toInternalBackstageCredentials(credentials);
|
||||
if (!token) {
|
||||
throw new Error('User credentials is unexpectedly missing token');
|
||||
}
|
||||
|
||||
// TODO: Proper refresh and expiration handling
|
||||
const expires = decodeJwt(token).exp!;
|
||||
@@ -149,6 +171,7 @@ class DefaultHttpAuthService implements HttpAuthService {
|
||||
priority: 'high',
|
||||
sameSite: 'lax', // TBD
|
||||
});
|
||||
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -15,11 +15,11 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
BackstageUserCredentials,
|
||||
UserInfoService,
|
||||
BackstageUserInfo,
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
BackstageCredentials,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { toInternalBackstageCredentials } from '../auth/authServiceFactory';
|
||||
import { decodeJwt } from 'jose';
|
||||
@@ -27,12 +27,15 @@ import { decodeJwt } from 'jose';
|
||||
// TODO: The intention is for this to eventually be replaced by a call to the auth-backend
|
||||
export class DefaultUserInfoService implements UserInfoService {
|
||||
async getUserInfo(
|
||||
credentials: BackstageUserCredentials,
|
||||
credentials: BackstageCredentials,
|
||||
): Promise<BackstageUserInfo> {
|
||||
const internalCredentials = toInternalBackstageCredentials(credentials);
|
||||
if (internalCredentials.type !== 'user') {
|
||||
if (internalCredentials.principal.type !== 'user') {
|
||||
throw new Error('Only user credentials are supported');
|
||||
}
|
||||
if (!internalCredentials.token) {
|
||||
throw new Error('User credentials is unexpectedly missing token');
|
||||
}
|
||||
const { sub: userEntityRef, ent: ownershipEntityRefs = [] } = decodeJwt(
|
||||
internalCredentials.token,
|
||||
);
|
||||
|
||||
@@ -22,7 +22,12 @@ export interface AuthService {
|
||||
// (undocumented)
|
||||
authenticate(token: string): Promise<BackstageCredentials>;
|
||||
// (undocumented)
|
||||
issueServiceToken(options?: { forward?: BackstageCredentials }): Promise<{
|
||||
isPrincipal<TType extends keyof BackstagePrincipalTypes>(
|
||||
credentials: BackstageCredentials,
|
||||
type: TType,
|
||||
): credentials is BackstageCredentials<BackstagePrincipalTypes[TType]>;
|
||||
// (undocumented)
|
||||
issueToken(options: { forward?: BackstageCredentials }): Promise<{
|
||||
token: string;
|
||||
}>;
|
||||
}
|
||||
@@ -89,36 +94,35 @@ export interface BackendPluginRegistrationPoints {
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type BackstageCredentials =
|
||||
| BackstageUserCredentials
|
||||
| BackstageServiceCredentials;
|
||||
|
||||
// @public (undocumented)
|
||||
export type BackstageCredentialTypes = {
|
||||
user: BackstageUserCredentials;
|
||||
'user-cookie': BackstageUserCredentials;
|
||||
service: BackstageServiceCredentials;
|
||||
unauthorized: BackstageUnauthorizedCredentials;
|
||||
export type BackstageCredentials<TPrincipal = unknown> = {
|
||||
$$type: '@backstage/BackstageCredentials';
|
||||
principal: TPrincipal;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type BackstageServiceCredentials = {
|
||||
$$type: '@backstage/BackstageCredentials';
|
||||
export type BackstageHttpAccessToPrincipalTypesMapping = {
|
||||
user: BackstageUserPrincipal;
|
||||
'user-cookie': BackstageUserPrincipal;
|
||||
service: BackstageServicePrincipal;
|
||||
unauthenticated: BackstageNonePrincipal;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type BackstageNonePrincipal = {
|
||||
type: 'none';
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type BackstagePrincipalTypes = {
|
||||
user: BackstageUserPrincipal;
|
||||
service: BackstageServicePrincipal;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type BackstageServicePrincipal = {
|
||||
type: 'service';
|
||||
subject: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type BackstageUnauthorizedCredentials = {
|
||||
$$type: '@backstage/BackstageCredentials';
|
||||
type: 'unauthorized';
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type BackstageUserCredentials = {
|
||||
$$type: '@backstage/BackstageCredentials';
|
||||
type: 'user';
|
||||
userEntityRef: string;
|
||||
permissions?: string[];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -129,6 +133,12 @@ export interface BackstageUserInfo {
|
||||
userEntityRef: string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type BackstageUserPrincipal = {
|
||||
type: 'user';
|
||||
userEntityRef: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface CacheService {
|
||||
delete(key: string): Promise<void>;
|
||||
@@ -281,14 +291,22 @@ export interface ExtensionPointConfig {
|
||||
// @public (undocumented)
|
||||
export interface HttpAuthService {
|
||||
// (undocumented)
|
||||
createHttpPluginRouterMiddleware(): Handler;
|
||||
// (undocumented)
|
||||
credentials<TAllowed extends keyof BackstageCredentialTypes>(
|
||||
credentials<
|
||||
TAllowed extends
|
||||
| keyof BackstageHttpAccessToPrincipalTypesMapping
|
||||
| undefined = undefined,
|
||||
>(
|
||||
req: Request_2,
|
||||
options: {
|
||||
options?: {
|
||||
allow: Array<TAllowed>;
|
||||
},
|
||||
): Promise<BackstageCredentialTypes[TAllowed]>;
|
||||
): Promise<
|
||||
BackstageCredentials<
|
||||
TAllowed extends keyof BackstageHttpAccessToPrincipalTypesMapping
|
||||
? BackstageHttpAccessToPrincipalTypesMapping[TAllowed]
|
||||
: unknown
|
||||
>
|
||||
>;
|
||||
// (undocumented)
|
||||
issueUserCookie(res: Response_2): Promise<void>;
|
||||
// (undocumented)
|
||||
@@ -547,8 +565,6 @@ export interface UrlReaderService {
|
||||
// @public (undocumented)
|
||||
export interface UserInfoService {
|
||||
// (undocumented)
|
||||
getUserInfo(
|
||||
credentials: BackstageUserCredentials,
|
||||
): Promise<BackstageUserInfo>;
|
||||
getUserInfo(credentials: BackstageCredentials): Promise<BackstageUserInfo>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type BackstageUserCredentials = {
|
||||
$$type: '@backstage/BackstageCredentials';
|
||||
|
||||
export type BackstageUserPrincipal = {
|
||||
type: 'user';
|
||||
|
||||
userEntityRef: string;
|
||||
@@ -28,27 +26,54 @@ export type BackstageUserCredentials = {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type BackstageServiceCredentials = {
|
||||
$$type: '@backstage/BackstageCredentials';
|
||||
|
||||
type: 'service';
|
||||
|
||||
subject: string;
|
||||
export type BackstageNonePrincipal = {
|
||||
type: 'none';
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type BackstageCredentials =
|
||||
| BackstageUserCredentials
|
||||
| BackstageServiceCredentials;
|
||||
export type BackstageServicePrincipal = {
|
||||
type: 'service';
|
||||
|
||||
// Exact format TBD, possibly 'plugin:<pluginId>' or 'external:<externalServiceId>'
|
||||
subject: string;
|
||||
|
||||
// Not implemented in the first iteration, but this is how we might extend this in the future
|
||||
permissions?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type BackstageCredentials<TPrincipal = unknown> = {
|
||||
$$type: '@backstage/BackstageCredentials';
|
||||
|
||||
principal: TPrincipal;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type BackstagePrincipalTypes = {
|
||||
user: BackstageUserPrincipal;
|
||||
service: BackstageServicePrincipal;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface AuthService {
|
||||
authenticate(token: string): Promise<BackstageCredentials>;
|
||||
issueServiceToken(options?: {
|
||||
|
||||
isPrincipal<TType extends keyof BackstagePrincipalTypes>(
|
||||
credentials: BackstageCredentials,
|
||||
type: TType,
|
||||
): credentials is BackstageCredentials<BackstagePrincipalTypes[TType]>;
|
||||
|
||||
// TODO: should the caller provide the target plugin ID?
|
||||
// TODO: how can we make it very difficult to forget to forward credentials
|
||||
issueToken(options: {
|
||||
forward?: BackstageCredentials;
|
||||
}): Promise<{ token: string }>;
|
||||
}
|
||||
|
||||
@@ -16,36 +16,39 @@
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import {
|
||||
BackstageUserCredentials,
|
||||
BackstageServiceCredentials,
|
||||
BackstageCredentials,
|
||||
BackstageServicePrincipal,
|
||||
BackstageNonePrincipal,
|
||||
BackstageUserPrincipal,
|
||||
} from './AuthService';
|
||||
|
||||
/** @public */
|
||||
export type BackstageUnauthorizedCredentials = {
|
||||
$$type: '@backstage/BackstageCredentials';
|
||||
|
||||
type: 'unauthorized';
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type BackstageCredentialTypes = {
|
||||
user: BackstageUserCredentials;
|
||||
'user-cookie': BackstageUserCredentials;
|
||||
service: BackstageServiceCredentials;
|
||||
unauthorized: BackstageUnauthorizedCredentials;
|
||||
export type BackstageHttpAccessToPrincipalTypesMapping = {
|
||||
user: BackstageUserPrincipal;
|
||||
'user-cookie': BackstageUserPrincipal;
|
||||
service: BackstageServicePrincipal;
|
||||
unauthenticated: BackstageNonePrincipal;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export interface HttpAuthService {
|
||||
credentials<TAllowed extends keyof BackstageCredentialTypes>(
|
||||
credentials<
|
||||
TAllowed extends
|
||||
| keyof BackstageHttpAccessToPrincipalTypesMapping
|
||||
| undefined = undefined,
|
||||
>(
|
||||
req: Request,
|
||||
options: {
|
||||
options?: {
|
||||
allow: Array<TAllowed>;
|
||||
},
|
||||
): Promise<BackstageCredentialTypes[TAllowed]>;
|
||||
): Promise<
|
||||
BackstageCredentials<
|
||||
TAllowed extends keyof BackstageHttpAccessToPrincipalTypesMapping
|
||||
? BackstageHttpAccessToPrincipalTypesMapping[TAllowed]
|
||||
: unknown
|
||||
>
|
||||
>;
|
||||
|
||||
// TODO: Keep an eye on this, might not be needed
|
||||
requestHeaders(options?: {
|
||||
forward?: BackstageCredentials;
|
||||
}): Promise<Record<string, string>>;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackstageUserCredentials } from './AuthService';
|
||||
import { BackstageCredentials } from './AuthService';
|
||||
|
||||
/** @public */
|
||||
export interface BackstageUserInfo {
|
||||
@@ -24,7 +24,5 @@ export interface BackstageUserInfo {
|
||||
|
||||
/** @public */
|
||||
export interface UserInfoService {
|
||||
getUserInfo(
|
||||
credentials: BackstageUserCredentials,
|
||||
): Promise<BackstageUserInfo>;
|
||||
getUserInfo(credentials: BackstageCredentials): Promise<BackstageUserInfo>;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ export { coreServices } from './coreServices';
|
||||
export type {
|
||||
AuthService,
|
||||
BackstageCredentials,
|
||||
BackstageServiceCredentials,
|
||||
BackstageUserCredentials,
|
||||
BackstageUserPrincipal,
|
||||
BackstageServicePrincipal,
|
||||
BackstagePrincipalTypes,
|
||||
BackstageNonePrincipal,
|
||||
} from './AuthService';
|
||||
export type {
|
||||
CacheService,
|
||||
@@ -35,8 +37,7 @@ export type {
|
||||
} from './HttpRouterService';
|
||||
export type {
|
||||
HttpAuthService,
|
||||
BackstageCredentialTypes,
|
||||
BackstageUnauthorizedCredentials,
|
||||
BackstageHttpAccessToPrincipalTypesMapping,
|
||||
} from './HttpAuthService';
|
||||
export type {
|
||||
LifecycleService,
|
||||
|
||||
Reference in New Issue
Block a user