diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index f94979f975..ece34959de 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -21,7 +21,9 @@ import { AuthProviderInfo } from '@backstage/core-plugin-api'; import { AuthRequestOptions } from '@backstage/core-plugin-api'; import { BackstageIdentity } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; +import { BackstageIdentityResponse } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { BackstageUserIdentity } from '@backstage/core-plugin-api'; import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; @@ -388,6 +390,41 @@ export type FlatRoutesProps = { children: ReactNode; }; +// @public +export class GcpIapIdentity implements IdentityApi { + constructor(session: GcpIapSession); + static fromResponse(data: unknown): GcpIapIdentity; + // (undocumented) + getBackstageIdentity(): Promise; + // (undocumented) + getCredentials(): Promise<{ + token?: string | undefined; + }>; + // (undocumented) + getIdToken(): Promise; + // (undocumented) + getProfile(): ProfileInfo; + // (undocumented) + getProfileInfo(): Promise; + // (undocumented) + getUserId(): string; + // (undocumented) + signOut(): Promise; +} + +// @public +export type GcpIapSession = { + providerInfo: { + iapToken: { + sub: string; + email: string; + [key: string]: unknown; + }; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentityResponse; +}; + // @public export class GithubAuth implements OAuthApi, SessionApi { // (undocumented) diff --git a/packages/core-app-api/src/apis/implementations/auth/gcp-iap/GcpIapIdentity.ts b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/GcpIapIdentity.ts new file mode 100644 index 0000000000..0df14e1d99 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/GcpIapIdentity.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2021 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 { + BackstageUserIdentity, + IdentityApi, + ProfileInfo, +} from '@backstage/core-plugin-api'; +import { GcpIapSession, gcpIapSessionSchema } from './types'; + +/** + * An identity API based on a Google Identity-Aware Proxy auth response. + * + * @public + */ +export class GcpIapIdentity implements IdentityApi { + /** + * Creates an identity instance based on the auth-backend API response. + * + * @param data - The raw JSON response from the auth-backend. + */ + static fromResponse(data: unknown): GcpIapIdentity { + const parsed = gcpIapSessionSchema.parse(data); + return new GcpIapIdentity(parsed); + } + + constructor(private readonly session: GcpIapSession) {} + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getUserId} */ + getUserId(): string { + return this.session.backstageIdentity.id; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getIdToken} */ + async getIdToken(): Promise { + return this.session.backstageIdentity.token; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfile} */ + getProfile(): ProfileInfo { + return this.session.profile; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfileInfo} */ + async getProfileInfo(): Promise { + return this.session.profile; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getBackstageIdentity} */ + async getBackstageIdentity(): Promise { + return this.session.backstageIdentity.identity; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getCredentials} */ + async getCredentials(): Promise<{ token?: string | undefined }> { + return { token: this.session.backstageIdentity.token }; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.signOut} */ + async signOut(): Promise {} +} diff --git a/packages/core-app-api/src/apis/implementations/auth/gcp-iap/index.ts b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/index.ts new file mode 100644 index 0000000000..8133061f20 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 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 { GcpIapIdentity } from './GcpIapIdentity'; +export type { GcpIapSession } from './types'; diff --git a/packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.test.ts b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.test.ts new file mode 100644 index 0000000000..cb185103e2 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2021 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 { TypeOf } from 'zod'; +import { GcpIapSession, gcpIapSessionSchema } from './types'; + +describe('types', () => { + const responseData: GcpIapSession = { + providerInfo: { + iapToken: { + sub: 's', + email: 'e', + other: 7, + }, + }, + profile: { + email: 'e', + displayName: 'd', + picture: 'p', + }, + backstageIdentity: { + id: 'i', + token: 't', + identity: { + type: 'user', + userEntityRef: 'ue', + ownershipEntityRefs: ['oe'], + }, + }, + }; + + it('has a compatible schema type', () => { + function f(_b: TypeOf) {} + f(responseData); // no tsc errors + expect(gcpIapSessionSchema.parse(responseData)).toEqual(responseData); + }); +}); diff --git a/packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.ts b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.ts new file mode 100644 index 0000000000..98c728785d --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2021 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 { + BackstageIdentityResponse, + ProfileInfo, +} from '@backstage/core-plugin-api'; +import { z } from 'zod'; + +export const gcpIapSessionSchema = z.object({ + providerInfo: z.object({ + iapToken: z + .object({ + sub: z.string(), + email: z.string(), + }) + .catchall(z.unknown()), + }), + profile: z.object({ + email: z.string().optional(), + displayName: z.string().optional(), + picture: z.string().optional(), + }), + backstageIdentity: z.object({ + id: z.string(), + token: z.string(), + identity: z.object({ + type: z.literal('user'), + userEntityRef: z.string(), + ownershipEntityRefs: z.array(z.string()), + }), + }), +}); + +/** + * Session information for Google Identity-Aware Proxy auth. + * + * @public + */ +export type GcpIapSession = { + providerInfo: { + iapToken: { + sub: string; + email: string; + [key: string]: unknown; + }; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentityResponse; +}; diff --git a/packages/core-app-api/src/apis/implementations/auth/index.ts b/packages/core-app-api/src/apis/implementations/auth/index.ts index 50333f07a0..c53c9b7c76 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -25,4 +25,5 @@ export * from './microsoft'; export * from './onelogin'; export * from './bitbucket'; export * from './atlassian'; +export * from './gcp-iap'; export type { OAuthApiCreateOptions, AuthApiCreateOptions } from './types';