diff --git a/.changeset/curvy-owls-remember.md b/.changeset/curvy-owls-remember.md new file mode 100644 index 0000000000..7fd56073d6 --- /dev/null +++ b/.changeset/curvy-owls-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Updated the `SignInPage`, `ProxiedSignInPage` and `UserIdentity` implementations to match the removals of the deprecated `IdentityApi` methods and types. diff --git a/.changeset/fifty-horses-battle.md b/.changeset/fifty-horses-battle.md new file mode 100644 index 0000000000..c7164c7309 --- /dev/null +++ b/.changeset/fifty-horses-battle.md @@ -0,0 +1,24 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Removed deprecated `IdentityApi` methods: `getUserId`, `getIdToken`, and `getProfile`. + +Existing usage of `getUserId` can be replaced by `getBackstageIdentity`, more precisely the equivalent of the previous `userId` can be retrieved like this: + +```ts +import { parseEntityRef } from '@backstage/catalog-model'; + +const identity = await identityApi.getBackstageIdentity(); +const { name: userId } = parseEntityRef(identity.userEntityRef); +``` + +Note that it is recommended to consume the entire `userEntityRef` rather than parsing out just the name, in order to support namespaces. + +Existing usage of `getIdToken` can be replaced by `getCredentials`, like this: + +```ts +const { token } = await identityApi.getCredentials(); +``` + +And existing usage of `getProfile` is replaced by `getProfileInfo`, which returns the same profile object, but is now async. diff --git a/.changeset/mean-rings-burn.md b/.changeset/mean-rings-burn.md new file mode 100644 index 0000000000..2996de5c72 --- /dev/null +++ b/.changeset/mean-rings-burn.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-app-api': minor +'@backstage/core-plugin-api': minor +--- + +Removed deprecated `SignInResult` type, which was replaced with the new `onSignInSuccess` callback. diff --git a/.changeset/modern-buttons-draw.md b/.changeset/modern-buttons-draw.md new file mode 100644 index 0000000000..59a7849dcd --- /dev/null +++ b/.changeset/modern-buttons-draw.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Deprecated `loadIdentityOwnerRefs`, since they can now be retrieved as `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead. diff --git a/.changeset/new-mice-brush.md b/.changeset/new-mice-brush.md new file mode 100644 index 0000000000..b06050c88b --- /dev/null +++ b/.changeset/new-mice-brush.md @@ -0,0 +1,25 @@ +--- +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bazaar': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-permission-react': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-search': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-tech-insights': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-todo': patch +--- + +Migrated usage of deprecated `IdentityApi` methods. diff --git a/.changeset/poor-peaches-happen.md b/.changeset/poor-peaches-happen.md new file mode 100644 index 0000000000..db5dac5093 --- /dev/null +++ b/.changeset/poor-peaches-happen.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Removed the deprecated `id` field of `BackstageIdentityResponse`. + +Existing usage can be replaced by parsing the `name` of the `identity.userEntityRef` with `parseEntityRef` from `@backstage/catalog-model`, although note that it is recommended to consume the entire `userEntityRef` in order to support namespaces. diff --git a/.changeset/silver-crews-compare.md b/.changeset/silver-crews-compare.md new file mode 100644 index 0000000000..b211737e8a --- /dev/null +++ b/.changeset/silver-crews-compare.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Removed deprecated `BackstageIdentity` type, which was replaced by `BackstageIdentityResponse`. diff --git a/.changeset/wild-cows-add.md b/.changeset/wild-cows-add.md new file mode 100644 index 0000000000..1170bc494d --- /dev/null +++ b/.changeset/wild-cows-add.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Removed deprecated `OAuthRequestApi` types: `AuthProvider`, `AuthRequesterOptions`, `AuthRequester`, and `PendingAuthRequest`. diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index 803a6babc7..25dd08e054 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -218,7 +218,7 @@ export class MyApi implements MyInterface { async getMyData() { const backendUrl = this.configApi.getString('backend.baseUrl'); -+ const token = await this.identityApi.getIdToken(); ++ const { token } = await this.identityApi.getCredentials(); const requestUrl = `${backendUrl}/api/data/`; - const response = await fetch(requestUrl); + const response = await fetch( diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index af25679878..b813d1a9a5 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -19,8 +19,8 @@ import { atlassianAuthApiRef } from '@backstage/core-plugin-api'; import { auth0AuthApiRef } from '@backstage/core-plugin-api'; 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 { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; @@ -280,7 +280,7 @@ export type BitbucketSession = { expiresAt?: Date; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; // @public @@ -386,7 +386,7 @@ export class GithubAuth implements OAuthApi, SessionApi { // (undocumented) getBackstageIdentity( options?: AuthRequestOptions, - ): Promise; + ): Promise; // (undocumented) getProfile(options?: AuthRequestOptions): Promise; // (undocumented) @@ -407,7 +407,7 @@ export type GithubSession = { expiresAt?: Date; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; // @public @@ -465,7 +465,7 @@ export class OAuth2 // (undocumented) getBackstageIdentity( options?: AuthRequestOptions, - ): Promise; + ): Promise; // (undocumented) getIdToken(options?: AuthRequestOptions): Promise; // (undocumented) @@ -492,7 +492,7 @@ export type OAuth2Session = { expiresAt: Date; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; // @public @@ -540,7 +540,7 @@ export class SamlAuth // (undocumented) getBackstageIdentity( options?: AuthRequestOptions, - ): Promise; + ): Promise; // (undocumented) getProfile(options?: AuthRequestOptions): Promise; // (undocumented) @@ -555,7 +555,7 @@ export class SamlAuth export type SamlSession = { userId: string; profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; // @public @@ -563,14 +563,6 @@ export type SignInPageProps = { onSignInSuccess(identityApi: IdentityApi): void; }; -// @public @deprecated -export type SignInResult = { - userId: string; - profile: ProfileInfo; - getIdToken?: () => Promise; - signOut?: () => Promise; -}; - // @public export class UnhandledErrorForwarder { static forward(errorApi: ErrorApi, errorContext: ErrorApiErrorContext): void; diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.test.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.test.ts new file mode 100644 index 0000000000..aa15182499 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2020 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 { withLogCollector } from '@backstage//test-utils'; +import { AppIdentityProxy } from './AppIdentityProxy'; + +describe('AppIdentityProxy', () => { + const mockIdentityApi = { + getBackstageIdentity: jest.fn(), + getProfileInfo: jest.fn(), + getCredentials: jest.fn(), + signOut: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should forward user identities', async () => { + const proxy = new AppIdentityProxy(); + proxy.setTarget(mockIdentityApi); + + const logs = await withLogCollector(async () => { + mockIdentityApi.getBackstageIdentity.mockResolvedValueOnce({ + type: 'user', + userEntityRef: 'user:default/foo', + ownershipEntityRefs: [], + }); + await expect(proxy.getBackstageIdentity()).resolves.toEqual({ + type: 'user', + userEntityRef: 'user:default/foo', + ownershipEntityRefs: [], + }); + }); + + expect(logs).toEqual({ + log: [], + warn: [], + error: [], + }); + }); + + it('should warn about invalid user entity refs', async () => { + const proxy = new AppIdentityProxy(); + proxy.setTarget(mockIdentityApi); + + const logs = await withLogCollector(async () => { + mockIdentityApi.getBackstageIdentity.mockResolvedValueOnce({ + type: 'user', + userEntityRef: 'bar', + ownershipEntityRefs: [], + }); + await expect(proxy.getBackstageIdentity()).resolves.toEqual({ + type: 'user', + userEntityRef: 'bar', + ownershipEntityRefs: [], + }); + }); + + expect(logs).toEqual({ + log: [], + warn: [ + `WARNING: The App IdentityApi provided an invalid userEntityRef, 'bar'. ` + + `It must be a full Entity Reference of the form ':/'.`, + ], + error: [], + }); + }); +}); diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts index 8a2f12594c..5677299c83 100644 --- a/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts @@ -26,15 +26,30 @@ function mkError(thing: string) { ); } +function logDeprecation(thing: string) { + // eslint-disable-next-line no-console + console.warn( + `WARNING: Call to ${thing} is deprecated and will break in the future`, + ); +} + +// We use this for a period of backwards compatibility. It is a hidden +// compatibility that will allow old plugins to continue working for a limited time. +type CompatibilityIdentityApi = IdentityApi & { + getUserId?(): string; + getIdToken?(): Promise; + getProfile?(): ProfileInfo; +}; + /** * Implementation of the connection between the App-wide IdentityApi * and sign-in page. */ export class AppIdentityProxy implements IdentityApi { - private target?: IdentityApi; + private target?: CompatibilityIdentityApi; // This is called by the app manager once the sign-in page provides us with an implementation - setTarget(identityApi: IdentityApi) { + setTarget(identityApi: CompatibilityIdentityApi) { this.target = identityApi; } @@ -42,6 +57,10 @@ export class AppIdentityProxy implements IdentityApi { if (!this.target) { throw mkError('getUserId'); } + if (!this.target.getUserId) { + throw new Error('IdentityApi does not implement getUserId'); + } + logDeprecation('getUserId'); return this.target.getUserId(); } @@ -49,6 +68,10 @@ export class AppIdentityProxy implements IdentityApi { if (!this.target) { throw mkError('getProfile'); } + if (!this.target.getProfile) { + throw new Error('IdentityApi does not implement getProfile'); + } + logDeprecation('getProfile'); return this.target.getProfile(); } @@ -63,7 +86,16 @@ export class AppIdentityProxy implements IdentityApi { if (!this.target) { throw mkError('getBackstageIdentity'); } - return this.target.getBackstageIdentity(); + const identity = await this.target.getBackstageIdentity(); + if (!identity.userEntityRef.match(/^.*:.*\/.*$/)) { + // eslint-disable-next-line no-console + console.warn( + `WARNING: The App IdentityApi provided an invalid userEntityRef, '${identity.userEntityRef}'. ` + + `It must be a full Entity Reference of the form ':/'.`, + ); + } + + return identity; } async getCredentials(): Promise<{ token?: string | undefined }> { @@ -77,6 +109,10 @@ export class AppIdentityProxy implements IdentityApi { if (!this.target) { throw mkError('getIdToken'); } + if (!this.target.getIdToken) { + throw new Error('IdentityApi does not implement getIdToken'); + } + logDeprecation('getIdToken'); return this.target.getIdToken(); } diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts index c97cccc6ed..7c5d4f3fc0 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts @@ -15,7 +15,7 @@ */ import { - BackstageIdentity, + BackstageIdentityResponse, bitbucketAuthApiRef, ProfileInfo, } from '@backstage/core-plugin-api'; @@ -30,7 +30,7 @@ export type BitbucketAuthResponse = { expiresInSeconds: number; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; const DEFAULT_PROVIDER = { diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/types.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/types.ts index 5309fffcd8..6e62603408 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/types.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api'; +import { + ProfileInfo, + BackstageIdentityResponse, +} from '@backstage/core-plugin-api'; /** * Session information for Bitbucket auth. @@ -28,5 +31,5 @@ export type BitbucketSession = { expiresAt?: Date; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts index cb0f792524..dc2c15bc16 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -16,7 +16,7 @@ import { AuthRequestOptions, - BackstageIdentity, + BackstageIdentityResponse, OAuthApi, ProfileInfo, SessionApi, @@ -41,7 +41,7 @@ export type GithubAuthResponse = { expiresInSeconds?: number; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; const DEFAULT_PROVIDER = { @@ -145,7 +145,7 @@ export default class GithubAuth implements OAuthApi, SessionApi { async getBackstageIdentity( options: AuthRequestOptions = {}, - ): Promise { + ): Promise { const session = await this.sessionManager.getSession(options); return session?.backstageIdentity; } diff --git a/packages/core-app-api/src/apis/implementations/auth/github/types.ts b/packages/core-app-api/src/apis/implementations/auth/github/types.ts index 50b3ed871d..0ef662905f 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/types.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api'; +import { + ProfileInfo, + BackstageIdentityResponse, +} from '@backstage/core-plugin-api'; import { z } from 'zod'; // TODO(Rugvip): Make GithubSession internal @@ -33,7 +36,7 @@ export type GithubSession = { }; profile: ProfileInfo; // TODO(Rugvip): This should be made optional once the type is no longer public - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; export const githubSessionSchema: z.ZodSchema = z.object({ diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 18644a037f..ad2d04cb56 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -19,7 +19,7 @@ import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { AuthRequestOptions, - BackstageIdentity, + BackstageIdentityResponse, OAuthApi, OpenIdConnectApi, ProfileInfo, @@ -48,7 +48,7 @@ export type OAuth2Response = { expiresInSeconds: number; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; const DEFAULT_PROVIDER = { @@ -159,7 +159,7 @@ export default class OAuth2 async getBackstageIdentity( options: AuthRequestOptions = {}, - ): Promise { + ): Promise { const session = await this.sessionManager.getSession(options); return session?.backstageIdentity; } diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts index fb8b0e6c64..242cd94154 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api'; +import { + ProfileInfo, + BackstageIdentityResponse, +} from '@backstage/core-plugin-api'; export type { OAuth2CreateOptions } from './OAuth2'; /** @@ -30,5 +33,5 @@ export type OAuth2Session = { expiresAt: Date; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts index 98038b2f41..dad62f1d06 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -16,12 +16,12 @@ import { AuthRequestOptions, - BackstageIdentity, BackstageIdentityApi, ProfileInfo, ProfileInfoApi, SessionApi, SessionState, + BackstageIdentityResponse, } from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; import { DirectAuthConnector } from '../../../../lib/AuthConnector'; @@ -35,7 +35,7 @@ import { SamlSession, samlSessionSchema } from './types'; export type SamlAuthResponse = { profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; const DEFAULT_PROVIDER = { @@ -95,7 +95,7 @@ export default class SamlAuth async getBackstageIdentity( options: AuthRequestOptions = {}, - ): Promise { + ): Promise { const session = await this.sessionManager.getSession(options); return session?.backstageIdentity; } diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts index 3920e3ac86..f1345fd154 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { BackstageIdentity, ProfileInfo } from '@backstage/core-plugin-api'; +import { + BackstageIdentityResponse, + ProfileInfo, +} from '@backstage/core-plugin-api'; import { z } from 'zod'; /** @@ -26,13 +29,13 @@ import { z } from 'zod'; export type ExportedSamlSession = { userId: string; profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; /** @internal */ export type SamlSession = { profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity: BackstageIdentityResponse; }; /** @internal */ diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index bacaf83fbd..902e64c09f 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -18,7 +18,6 @@ import { ComponentType } from 'react'; import { AnyApiFactory, AppTheme, - ProfileInfo, IconComponent, BackstagePlugin, RouteRef, @@ -38,31 +37,6 @@ export type BootErrorPageProps = { error: Error; }; -/** - * The outcome of signing in on the sign-in page. - * - * @public - * @deprecated replaced by passing the {@link @backstage/core-plugin-api#IdentityApi} to the {@link SignInPageProps.onSignInSuccess} instead. - */ -export type SignInResult = { - /** - * User ID that will be returned by the IdentityApi - */ - userId: string; - - profile: ProfileInfo; - - /** - * Function used to retrieve an ID token for the signed in user. - */ - getIdToken?: () => Promise; - - /** - * Sign out handler that will be called if the user requests to sign out. - */ - signOut?: () => Promise; -}; - /** * Props for the `SignInPage` component of {@link AppComponents}. * diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 8b5d1c61a7..bfe1064fed 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -39,7 +39,6 @@ import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { SessionApi } from '@backstage/core-plugin-api'; import { SignInPageProps } from '@backstage/core-plugin-api'; -import { SignInResult } from '@backstage/core-plugin-api'; import { SparklinesLineProps } from 'react-sparklines'; import { SparklinesProps } from 'react-sparklines'; import { StyledComponentProps } from '@material-ui/core/styles'; @@ -2424,7 +2423,12 @@ export class UserIdentity implements IdentityApi { profile?: ProfileInfo; }): IdentityApi; static createGuest(): IdentityApi; - static fromLegacy(result: SignInResult): IdentityApi; + static fromLegacy(result: { + userId: string; + profile: ProfileInfo; + getIdToken?: () => Promise; + signOut?: () => Promise; + }): IdentityApi; // (undocumented) getBackstageIdentity(): Promise; // (undocumented) diff --git a/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts b/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts index ec45bc7399..396913b9b7 100644 --- a/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts +++ b/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts @@ -20,30 +20,46 @@ import { ProfileInfo, } from '@backstage/core-plugin-api'; +// Similar to the AppIdentityApi we provide backwards compatibility for a limited time +type CompatibilityIdentityApi = IdentityApi & { + getUserId?(): string; + getIdToken?(): Promise; + getProfile?(): ProfileInfo; +}; + export class IdentityApiSignOutProxy implements IdentityApi { private constructor( private readonly config: { - identityApi: IdentityApi; + identityApi: CompatibilityIdentityApi; signOut: IdentityApi['signOut']; }, ) {} static from(config: { - identityApi: IdentityApi; + identityApi: CompatibilityIdentityApi; signOut: IdentityApi['signOut']; }): IdentityApi { return new IdentityApiSignOutProxy(config); } getUserId(): string { + if (!this.config.identityApi.getUserId) { + throw new Error(`SignOutProxy IdentityApi.getUserId is not implemented`); + } return this.config.identityApi.getUserId(); } getIdToken(): Promise { + if (!this.config.identityApi.getIdToken) { + throw new Error(`SignOutProxy IdentityApi.getIdToken is not implemented`); + } return this.config.identityApi.getIdToken(); } getProfile(): ProfileInfo { + if (!this.config.identityApi.getProfile) { + throw new Error(`SignOutProxy IdentityApi.getProfile is not implemented`); + } return this.config.identityApi.getProfile(); } diff --git a/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts b/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts index e28f94072e..cc51d2b9a8 100644 --- a/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts @@ -18,7 +18,6 @@ import { IdentityApi, ProfileInfo, BackstageUserIdentity, - SignInResult, } from '@backstage/core-plugin-api'; function parseJwtPayload(token: string) { @@ -26,14 +25,22 @@ function parseJwtPayload(token: string) { return JSON.parse(atob(payload)); } +type LegacySignInResult = { + userId: string; + profile: ProfileInfo; + getIdToken?: () => Promise; + signOut?: () => Promise; +}; + +/** @internal */ export class LegacyUserIdentity implements IdentityApi { - private constructor(private readonly result: SignInResult) {} + private constructor(private readonly result: LegacySignInResult) {} getUserId(): string { return this.result.userId; } - static fromResult(result: SignInResult): LegacyUserIdentity { + static fromResult(result: LegacySignInResult): LegacyUserIdentity { return new LegacyUserIdentity(result); } diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.ts index 7781c79154..781cb47c0e 100644 --- a/packages/core-components/src/layout/SignInPage/UserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.ts @@ -21,12 +21,17 @@ import { BackstageUserIdentity, BackstageIdentityApi, SessionApi, - SignInResult, } from '@backstage/core-plugin-api'; import { GuestUserIdentity } from './GuestUserIdentity'; import { LegacyUserIdentity } from './LegacyUserIdentity'; +// TODO(Rugvip): This and the other IdentityApi implementations still implement +// the old removed methods. This is to allow for backwards compatibility +// with old plugins that still consume this API. We will leave these in +// place as a hidden compatibility for a couple of months. +// The AppIdentityProxy warns in case any of these methods are called. + /** * An implementation of the IdentityApi that is constructed using * various backstage user identity representations. @@ -49,7 +54,24 @@ export class UserIdentity implements IdentityApi { * * @public */ - static fromLegacy(result: SignInResult): IdentityApi { + static fromLegacy(result: { + /** + * User ID that will be returned by the IdentityApi + */ + userId: string; + + profile: ProfileInfo; + + /** + * Function used to retrieve an ID token for the signed in user. + */ + getIdToken?: () => Promise; + + /** + * Sign out handler that will be called if the user requests to sign out. + */ + signOut?: () => Promise; + }): IdentityApi { return LegacyUserIdentity.fromResult(result); } diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index de25d24660..5ac07e195c 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -12,7 +12,6 @@ import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api'; import { JsonValue } from '@backstage/types'; import { Observable } from '@backstage/types'; -import { ProfileInfo as ProfileInfo_2 } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; @@ -195,9 +194,6 @@ export const auth0AuthApiRef: ApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; -// @public @deprecated (undocumented) -export type AuthProvider = Omit; - // @public export type AuthProviderInfo = { id: string; @@ -205,21 +201,12 @@ export type AuthProviderInfo = { icon: IconComponent; }; -// @public @deprecated (undocumented) -export type AuthRequester = OAuthRequester; - -// @public @deprecated (undocumented) -export type AuthRequesterOptions = OAuthRequesterOptions; - // @public export type AuthRequestOptions = { optional?: boolean; instantPopup?: boolean; }; -// @public @deprecated -export type BackstageIdentity = BackstageIdentityResponse; - // @public export type BackstageIdentityApi = { getBackstageIdentity( @@ -229,7 +216,6 @@ export type BackstageIdentityApi = { // @public export type BackstageIdentityResponse = { - id: string; token: string; identity: BackstageUserIdentity; }; @@ -512,9 +498,6 @@ export type IconComponent = ComponentType<{ // @public export type IdentityApi = { - getUserId(): string; - getIdToken(): Promise; - getProfile(): ProfileInfo; getProfileInfo(): Promise; getBackstageIdentity(): Promise; getCredentials(): Promise<{ @@ -657,9 +640,6 @@ export type PathParams = { [name in ParamNames]: string; }; -// @public @deprecated (undocumented) -export type PendingAuthRequest = PendingOAuthRequest; - // @public export type PendingOAuthRequest = { provider: Omit & { @@ -732,14 +712,6 @@ export type SignInPageProps = { onSignInSuccess(identityApi: IdentityApi_2): void; }; -// @public @deprecated -export type SignInResult = { - userId: string; - profile: ProfileInfo_2; - getIdToken?: () => Promise; - signOut?: () => Promise; -}; - // @public export interface StorageApi { forBucket(name: string): StorageApi; diff --git a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts index fe4de89106..1b127a1971 100644 --- a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts @@ -22,31 +22,6 @@ import { BackstageUserIdentity, ProfileInfo } from './auth'; * @public */ export type IdentityApi = { - /** - * The ID of the signed in user. This ID is not meant to be presented to the user, but used - * as an opaque string to pass on to backends or use in frontend logic. - * - * @deprecated use {@link IdentityApi.getBackstageIdentity} instead. - */ - getUserId(): string; - - /** - * An OpenID Connect ID Token which proves the identity of the signed in user. - * - * The ID token will be undefined if the signed in user does not have a verified - * identity, such as a demo user or mocked user for e2e tests. - * - * @deprecated use {@link IdentityApi.getCredentials} instead. - */ - getIdToken(): Promise; - - /** - * The profile of the signed in user. - * - * @deprecated use {@link IdentityApi.getProfileInfo} instead. - */ - getProfile(): ProfileInfo; - /** * The profile of the signed in user. */ diff --git a/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts b/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts index 5b8dab29f7..ccca2c50e3 100644 --- a/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts @@ -18,12 +18,6 @@ import { Observable } from '@backstage/types'; import { ApiRef, createApiRef } from '../system'; import { AuthProviderInfo } from './auth'; -/** - * @public - * @deprecated Use AuthProviderInfo instead - */ -export type AuthProvider = Omit; - /** * Describes how to handle auth requests. Both how to show them to the user, and what to do when * the user accesses the auth request. @@ -45,12 +39,6 @@ export type OAuthRequesterOptions = { onAuthRequest(scopes: Set): Promise; }; -/** - * @public - * @deprecated Use OAuthRequesterOptions instead - */ -export type AuthRequesterOptions = OAuthRequesterOptions; - /** * Function used to trigger new auth requests for a set of scopes. * @@ -69,12 +57,6 @@ export type OAuthRequester = ( scopes: Set, ) => Promise; -/** - * @public - * @deprecated Use OAuthRequester instead - */ -export type AuthRequester = OAuthRequester; - /** * An pending auth request for a single auth provider. The request will remain in this pending * state until either reject() or trigger() is called. @@ -107,12 +89,6 @@ export type PendingOAuthRequest = { trigger(): Promise; }; -/** - * @public - * @deprecated Use PendingOAuthRequest instead - */ -export type PendingAuthRequest = PendingOAuthRequest; - /** * Provides helpers for implemented OAuth login flows within Backstage. * diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index a362d16750..ed5a7a3264 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -221,13 +221,6 @@ export type BackstageUserIdentity = { * @public */ export type BackstageIdentityResponse = { - /** - * The backstage user ID. - * - * @deprecated The identity is now provided via the `identity` field instead. - */ - id: string; - /** * The token used to authenticate the user within Backstage. */ @@ -239,14 +232,6 @@ export type BackstageIdentityResponse = { identity: BackstageUserIdentity; }; -/** - * The old exported symbol for {@link BackstageIdentityResponse}. - * - * @public - * @deprecated use {@link BackstageIdentityResponse} instead. - */ -export type BackstageIdentity = BackstageIdentityResponse; - /** * Profile information of the user. * diff --git a/packages/core-plugin-api/src/app/types.ts b/packages/core-plugin-api/src/app/types.ts index df78728a21..2a71eda1f4 100644 --- a/packages/core-plugin-api/src/app/types.ts +++ b/packages/core-plugin-api/src/app/types.ts @@ -20,7 +20,6 @@ // eslint-disable-next-line no-restricted-imports export type { BootErrorPageProps, - SignInResult, SignInPageProps, ErrorBoundaryFallbackProps, AppComponents, diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts index 42c5ac3f6c..84ade8800d 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -96,7 +96,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { const baseUrl = `${await this.discoveryApi.getBaseUrl('azure-devops')}/`; const url = new URL(path, baseUrl); - const idToken = await this.identityApi.getIdToken(); + const { token: idToken } = await this.identityApi.getCredentials(); const response = await fetch(url.toString(), { headers: idToken ? { Authorization: `Bearer ${idToken}` } : {}, }); diff --git a/plugins/azure-devops/src/hooks/useUserEmail.ts b/plugins/azure-devops/src/hooks/useUserEmail.ts index 4655815297..34f39f4db9 100644 --- a/plugins/azure-devops/src/hooks/useUserEmail.ts +++ b/plugins/azure-devops/src/hooks/useUserEmail.ts @@ -15,8 +15,10 @@ */ import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; export function useUserEmail(): string | undefined { const identityApi = useApi(identityApiRef); - return identityApi.getProfile().email; + const state = useAsync(() => identityApi.getProfileInfo(), [identityApi]); + return state.value?.email; } diff --git a/plugins/badges/src/api/BadgesClient.ts b/plugins/badges/src/api/BadgesClient.ts index 8ad5d313e4..68714b2fa4 100644 --- a/plugins/badges/src/api/BadgesClient.ts +++ b/plugins/badges/src/api/BadgesClient.ts @@ -34,7 +34,7 @@ export class BadgesClient implements BadgesApi { public async getEntityBadgeSpecs(entity: Entity): Promise { const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity); - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const response = await fetch(entityBadgeSpecsUrl, { headers: token ? { diff --git a/plugins/bazaar/src/api.ts b/plugins/bazaar/src/api.ts index cae890ea4a..2d661506bc 100644 --- a/plugins/bazaar/src/api.ts +++ b/plugins/bazaar/src/api.ts @@ -121,6 +121,7 @@ export class BazaarClient implements BazaarApi { async addMember(id: number, userId: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); + const { picture } = await this.identityApi.getProfileInfo(); await fetch( `${baseUrl}/projects/${encodeURIComponent( @@ -132,9 +133,7 @@ export class BazaarClient implements BazaarApi { Accept: 'application/json', 'Content-Type': 'application/json', }, - body: JSON.stringify({ - picture: (await this.identityApi.getProfileInfo()).picture, - }), + body: JSON.stringify({ picture }), }, ); } diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 6154eec757..202a7aba85 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -67,21 +67,12 @@ describe('CatalogImportClient', () => { getCredentials: jest.fn().mockResolvedValue({ token: 'token' }), }; const identityApi = { - getUserId: () => { - return 'user'; - }, - getProfile: () => { - return {}; - }, - getIdToken: () => { - return Promise.resolve('token'); - }, signOut: () => { return Promise.resolve(); }, getProfileInfo: jest.fn(), getBackstageIdentity: jest.fn(), - getCredentials: jest.fn(), + getCredentials: jest.fn().mockResolvedValue({ token: 'token' }), }; const scmIntegrationsApi = ScmIntegrations.fromConfig( diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index 752899e6e8..bc59657cad 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -173,13 +173,13 @@ the component will become available.\n\nFor more information, read an \ }: { repo: string; }): Promise { - const idToken = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`, { headers: { 'Content-Type': 'application/json', - ...(idToken && { Authorization: `Bearer ${idToken}` }), + ...(token && { Authorization: `Bearer ${token}` }), }, method: 'POST', body: JSON.stringify({ diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index ec0062efeb..ac1c086f72 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -781,7 +781,7 @@ export function loadCatalogOwnerRefs( identityOwnerRefs: string[], ): Promise; -// @public +// @public @deprecated export function loadIdentityOwnerRefs( identityApi: IdentityApi, ): Promise; diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index fd5cf3b9dd..a1e722bc6d 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -68,8 +68,12 @@ const mockConfigApi = { getOptionalString: () => '', } as Partial; const mockIdentityApi: Partial = { - getUserId: () => 'guest', - getIdToken: async () => undefined, + getBackstageIdentity: async () => ({ + type: 'user', + userEntityRef: 'user:default/guest', + ownershipEntityRefs: [], + }), + getCredentials: async () => ({ token: undefined }), }; const mockCatalogApi: Partial = { getEntities: jest.fn().mockImplementation(async () => ({ items: entities })), diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx index f01c13839d..7f838163a2 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx @@ -33,14 +33,11 @@ import { } from './useEntityOwnership'; describe('useEntityOwnership', () => { - type MockIdentityApi = jest.Mocked< - Pick - >; + type MockIdentityApi = jest.Mocked>; type MockCatalogApi = jest.Mocked>; const mockIdentityApi: MockIdentityApi = { - getUserId: jest.fn(), - getIdToken: jest.fn(), + getBackstageIdentity: jest.fn(), }; const mockCatalogApi: MockCatalogApi = { getEntityByName: jest.fn(), @@ -100,80 +97,19 @@ describe('useEntityOwnership', () => { ], }; - // these were generated on https://jwt.io, based off of its default example token - // no ent at all - const tokenNoEnt = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; - // "ent": [] - const tokenEmptyEnt = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJlbnQiOltdfQ.Khyza2whczkoC4wSCLBhBaBB9-ktIkk7gpXEgQPHhtY'; - // "ent": ["user:default/user1"] - const tokenUserEnt = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJlbnQiOlsidXNlcjpkZWZhdWx0L3VzZXIxIl19.CMCxjwI4rj_TD3uUoBNgFjkZI23LwRTbQnSPBxzncoY'; - // "ent": ["user:default/user1", "group:default/group1"] - const tokenUserAndGroupEnt = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJlbnQiOlsidXNlcjpkZWZhdWx0L3VzZXIxIiwiZ3JvdXA6ZGVmYXVsdC9ncm91cDEiXX0.ZZmZrogbQKx0hnForw63ETkyAhUyeoBE8Hgloi45rdg'; - afterEach(() => { jest.resetAllMocks(); }); describe('loadIdentityOwnerRefs', () => { - it('returns the user id when there is no relevant token info', async () => { - mockIdentityApi.getUserId.mockReturnValueOnce('foo'); - mockIdentityApi.getIdToken.mockResolvedValueOnce(undefined); - await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([ - 'user:default/foo', - ]); - - mockIdentityApi.getUserId.mockReturnValueOnce('ns/foo'); - mockIdentityApi.getIdToken.mockResolvedValueOnce(undefined); - await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([ - 'user:ns/foo', - ]); - - mockIdentityApi.getUserId.mockReturnValueOnce('user:ns/foo'); - mockIdentityApi.getIdToken.mockResolvedValueOnce(undefined); - await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([ - 'user:ns/foo', - ]); - - mockIdentityApi.getUserId.mockReturnValueOnce('foo'); - mockIdentityApi.getIdToken.mockResolvedValueOnce(tokenNoEnt); - await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([ - 'user:default/foo', - ]); - - mockIdentityApi.getUserId.mockReturnValueOnce('foo'); - mockIdentityApi.getIdToken.mockResolvedValueOnce(tokenEmptyEnt); - await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([ - 'user:default/foo', - ]); - }); - - it('returns both the user id and the token parts', async () => { - mockIdentityApi.getUserId.mockReturnValueOnce('foo'); - mockIdentityApi.getIdToken.mockResolvedValueOnce(tokenUserEnt); - await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([ - 'user:default/foo', - 'user:default/user1', - ]); - - mockIdentityApi.getUserId.mockReturnValueOnce('foo'); - mockIdentityApi.getIdToken.mockResolvedValueOnce(tokenUserAndGroupEnt); - await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([ - 'user:default/foo', - 'user:default/user1', - 'group:default/group1', - ]); - }); - - it('gracefully ignores broken token', async () => { - mockIdentityApi.getUserId.mockReturnValueOnce('foo'); - mockIdentityApi.getIdToken.mockResolvedValueOnce('not a jwt'); - await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([ - 'user:default/foo', - ]); + it('passes through the ownershipEntityRefs', async () => { + const refs = new Array(); + mockIdentityApi.getBackstageIdentity.mockResolvedValueOnce({ + type: 'user', + userEntityRef: 'user:default/guest', + ownershipEntityRefs: refs, + }); + await expect(loadIdentityOwnerRefs(identityApi)).resolves.toBe(refs); }); }); @@ -204,9 +140,12 @@ describe('useEntityOwnership', () => { }); describe('useEntityOwnership', () => { - it('matches ownership via token claims', async () => { - mockIdentityApi.getUserId.mockReturnValue('foo'); - mockIdentityApi.getIdToken.mockResolvedValue(tokenUserAndGroupEnt); + it('matches ownership via ownership entity refs', async () => { + mockIdentityApi.getBackstageIdentity.mockResolvedValue({ + type: 'user', + userEntityRef: 'user:default/user1', + ownershipEntityRefs: ['user:default/user1', 'group:default/group1'], + }); mockCatalogApi.getEntityByName.mockResolvedValue(undefined); const { result, waitForValueToChange } = renderHook( @@ -224,32 +163,5 @@ describe('useEntityOwnership', () => { expect(result.current.loading).toBe(false); expect(result.current.isOwnedEntity(ownedEntity)).toBe(true); }); - - it('matches ownership via catalog user entity', async () => { - mockIdentityApi.getUserId.mockReturnValue('user2'); - mockIdentityApi.getIdToken.mockResolvedValue(undefined); - mockCatalogApi.getEntityByName.mockResolvedValue(user2Entity); - - const { result, waitForValueToChange } = renderHook( - () => useEntityOwnership(), - { - wrapper: Wrapper, - }, - ); - - expect(result.current.loading).toBe(true); - expect(result.current.isOwnedEntity(ownedEntity)).toBe(false); - - await waitForValueToChange(() => result.current.loading); - - expect(result.current.loading).toBe(false); - expect(result.current.isOwnedEntity(ownedEntity)).toBe(true); - - expect(mockCatalogApi.getEntityByName).toBeCalledWith({ - kind: 'user', - namespace: 'default', - name: 'user2', - }); - }); }); }); diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.ts b/plugins/catalog-react/src/hooks/useEntityOwnership.ts index 672b41cf33..1138dada1f 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.ts +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.ts @@ -28,33 +28,18 @@ import { identityApiRef, useApi, } from '@backstage/core-plugin-api'; -import jwtDecoder from 'jwt-decode'; import { useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '../api'; import { getEntityRelations } from '../utils/getEntityRelations'; -// Takes a user ID from the identity, which can be on basically any form, and -// returns an entity ref. E.g. if the input is "foo", it returns -// "user:default/foo" to make sure it's a full ref. -function extendUserId(id: string): string { - try { - const ref = parseEntityRef(id, { - defaultKind: 'User', - defaultNamespace: 'default', - }); - return stringifyEntityRef(ref); - } catch { - return id; - } -} - /** * Takes the relevant parts of the Backstage identity, and translates them into * a list of entity refs on string form that represent the user's ownership * connections. * * @public + * @deprecated Use `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead. * * @param identityApi - The IdentityApi implementation * @returns IdentityOwner refs as a string array @@ -62,30 +47,8 @@ function extendUserId(id: string): string { export async function loadIdentityOwnerRefs( identityApi: IdentityApi, ): Promise { - const id = identityApi.getUserId(); - const token = await identityApi.getIdToken(); - const result: string[] = []; - - if (id) { - result.push(extendUserId(id)); - } - - if (token) { - try { - const decoded = jwtDecoder(token) as any; - if (decoded?.ent) { - [decoded.ent] - .flat() - .filter(x => typeof x === 'string') - .map(x => x.toLocaleLowerCase('en-US')) - .forEach(x => result.push(x)); - } - } catch { - // ignore - } - } - - return result; + const identity = await identityApi.getBackstageIdentity(); + return identity.ownershipEntityRefs; } /** @@ -142,9 +105,12 @@ export function useEntityOwnership(): { // Trigger load only on mount const { loading, value: refs } = useAsync(async () => { - const identityRefs = await loadIdentityOwnerRefs(identityApi); - const catalogRefs = await loadCatalogOwnerRefs(catalogApi, identityRefs); - return new Set([...identityRefs, ...catalogRefs]); + const { ownershipEntityRefs } = await identityApi.getBackstageIdentity(); + const catalogRefs = await loadCatalogOwnerRefs( + catalogApi, + ownershipEntityRefs, + ); + return new Set([...ownershipEntityRefs, ...catalogRefs]); }, []); const isOwnedEntity = useMemo(() => { diff --git a/plugins/catalog-react/src/hooks/useOwnUser.ts b/plugins/catalog-react/src/hooks/useOwnUser.ts index 3df51f885f..f49eb768d0 100644 --- a/plugins/catalog-react/src/hooks/useOwnUser.ts +++ b/plugins/catalog-react/src/hooks/useOwnUser.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { UserEntity } from '@backstage/catalog-model'; +import { + ENTITY_DEFAULT_NAMESPACE, + parseEntityRef, + UserEntity, +} from '@backstage/catalog-model'; import useAsync, { AsyncState } from 'react-use/lib/useAsync'; import { catalogApiRef } from '../api'; import { identityApiRef, useApi } from '@backstage/core-plugin-api'; @@ -26,15 +30,13 @@ export function useOwnUser(): AsyncState { const catalogApi = useApi(catalogApiRef); const identityApi = useApi(identityApiRef); - // TODO: get the full entity (or at least the full entity name) from the - // identityApi - return useAsync( - () => - catalogApi.getEntityByName({ - kind: 'User', - namespace: 'default', - name: identityApi.getUserId(), - }) as Promise, - [catalogApi, identityApi], - ); + return useAsync(async () => { + const identity = await identityApi.getBackstageIdentity(); + return catalogApi.getEntityByName( + parseEntityRef(identity.userEntityRef, { + defaultKind: 'User', + defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + }), + ) as Promise; + }, [catalogApi, identityApi]); } diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts index 6917b5abed..e1bba9e60f 100644 --- a/plugins/catalog/src/CatalogClientWrapper.test.ts +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -28,38 +28,16 @@ const discoveryApi: DiscoveryApi = { }, }; const identityApi: IdentityApi = { - getUserId() { - return 'jane-fonda'; - }, - getProfile() { - return { email: 'jane-fonda@spotify.com' }; - }, - async getIdToken() { - return Promise.resolve('fake-id-token'); - }, - async signOut() { - return Promise.resolve(); - }, + signOut: jest.fn(), getProfileInfo: jest.fn(), getBackstageIdentity: jest.fn(), - getCredentials: jest.fn(), + getCredentials: jest.fn().mockResolvedValue({ token: 'fake-id-token' }), }; const guestIdentityApi: IdentityApi = { - getUserId() { - return 'guest'; - }, - getProfile() { - return {}; - }, - async getIdToken() { - return Promise.resolve(undefined); - }, - async signOut() { - return Promise.resolve(); - }, + signOut: jest.fn(), getProfileInfo: jest.fn(), getBackstageIdentity: jest.fn(), - getCredentials: jest.fn(), + getCredentials: jest.fn().mockResolvedValue({ token: undefined }), }; describe('CatalogClientWrapper', () => { diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts index c5f072e96c..939c4f095b 100644 --- a/plugins/catalog/src/CatalogClientWrapper.ts +++ b/plugins/catalog/src/CatalogClientWrapper.ts @@ -51,89 +51,108 @@ export class CatalogClientWrapper implements CatalogApi { id: string, options?: CatalogRequestOptions, ): Promise { - return await this.client.getLocationById(id, { - token: options?.token ?? (await this.identityApi.getIdToken()), - }); + return await this.client.getLocationById( + id, + await this.getCredentials(options), + ); } async getEntities( request?: CatalogEntitiesRequest, options?: CatalogRequestOptions, ): Promise> { - return await this.client.getEntities(request, { - token: options?.token ?? (await this.identityApi.getIdToken()), - }); + return await this.client.getEntities( + request, + await this.getCredentials(options), + ); } async getEntityByName( compoundName: EntityName, options?: CatalogRequestOptions, ): Promise { - return await this.client.getEntityByName(compoundName, { - token: options?.token ?? (await this.identityApi.getIdToken()), - }); + return await this.client.getEntityByName( + compoundName, + await this.getCredentials(options), + ); } async addLocation( request: AddLocationRequest, options?: CatalogRequestOptions, ): Promise { - return await this.client.addLocation(request, { - token: options?.token ?? (await this.identityApi.getIdToken()), - }); + return await this.client.addLocation( + request, + await this.getCredentials(options), + ); } async getOriginLocationByEntity( entity: Entity, options?: CatalogRequestOptions, ): Promise { - return await this.client.getOriginLocationByEntity(entity, { - token: options?.token ?? (await this.identityApi.getIdToken()), - }); + return await this.client.getOriginLocationByEntity( + entity, + await this.getCredentials(options), + ); } async getLocationByEntity( entity: Entity, options?: CatalogRequestOptions, ): Promise { - return await this.client.getLocationByEntity(entity, { - token: options?.token ?? (await this.identityApi.getIdToken()), - }); + return await this.client.getLocationByEntity( + entity, + await this.getCredentials(options), + ); } async removeLocationById( id: string, options?: CatalogRequestOptions, ): Promise { - return await this.client.removeLocationById(id, { - token: options?.token ?? (await this.identityApi.getIdToken()), - }); + return await this.client.removeLocationById( + id, + await this.getCredentials(options), + ); } async removeEntityByUid( uid: string, options?: CatalogRequestOptions, ): Promise { - return await this.client.removeEntityByUid(uid, { - token: options?.token ?? (await this.identityApi.getIdToken()), - }); + return await this.client.removeEntityByUid( + uid, + await this.getCredentials(options), + ); } async refreshEntity( entityRef: string, options?: CatalogRequestOptions, ): Promise { - return await this.client.refreshEntity(entityRef, { - token: options?.token ?? (await this.identityApi.getIdToken()), - }); + return await this.client.refreshEntity( + entityRef, + await this.getCredentials(options), + ); } async getEntityAncestors( request: CatalogEntityAncestorsRequest, options?: CatalogRequestOptions, ): Promise { - return await this.client.getEntityAncestors(request, { - token: options?.token ?? (await this.identityApi.getIdToken()), - }); + return await this.client.getEntityAncestors( + request, + await this.getCredentials(options), + ); + } + + private async getCredentials( + options?: CatalogRequestOptions, + ): Promise<{ token?: string }> { + if (options?.token) { + return { token: options?.token }; + } + return this.identityApi.getCredentials(); } } diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index 5f21ba364b..135a2a590d 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -120,9 +120,13 @@ describe('DefaultCatalogPage', () => { displayName: 'Display Name', }; const identityApi: Partial = { - getUserId: () => 'tools', - getIdToken: async () => undefined, - getProfile: () => testProfile, + getBackstageIdentity: async () => ({ + type: 'user', + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest', 'group:default/tools'], + }), + getCredentials: async () => ({ token: undefined }), + getProfileInfo: async () => testProfile, }; const storageApi = MockStorageApi.create(); diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 2e7ec2bad6..e24f45fd06 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -31,6 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/catalog-model": "^0.9.9", "@backstage/config": "^0.1.12", "@backstage/core-components": "^0.8.4", "@backstage/core-plugin-api": "^0.5.0", diff --git a/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx b/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx index e29bb78f2f..cccf8b60cd 100644 --- a/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx +++ b/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx @@ -23,7 +23,7 @@ import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; describe('', () => { const identityApi: Partial = { - getProfile: () => ({ + getProfileInfo: async () => ({ email: 'test-email@example.com', displayName: 'User 1', }), diff --git a/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.tsx b/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.tsx index 3d94ec19a2..cc68f79db1 100644 --- a/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.tsx +++ b/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.tsx @@ -16,16 +16,15 @@ import React from 'react'; import { Typography } from '@material-ui/core'; +import useAsync from 'react-use/lib/useAsync'; import { useCostInsightsStyles } from '../../utils/styles'; import { Group } from '../../types'; -import { - identityApiRef, - ProfileInfo, - useApi, -} from '@backstage/core-plugin-api'; +import { identityApiRef, useApi } from '@backstage/core-plugin-api'; -function name(profile: ProfileInfo | undefined): string { - return profile?.displayName || 'Mysterious Stranger'; +function useDisplayName(): string { + const identityApi = useApi(identityApiRef); + const state = useAsync(() => identityApi.getProfileInfo(), [identityApi]); + return state.loading ? '' : state.value?.displayName || 'Mysterious Stranger'; } type CostInsightsHeaderProps = { @@ -39,7 +38,7 @@ const CostInsightsHeaderNoData = ({ owner, groups, }: CostInsightsHeaderProps) => { - const profile = useApi(identityApiRef).getProfile(); + const displayName = useDisplayName(); const classes = useCostInsightsStyles(); const hasMultipleGroups = groups.length > 1; @@ -52,8 +51,8 @@ const CostInsightsHeaderNoData = ({ Well this is awkward - Hey, {name(profile)}! {owner} doesn't seem to have any - cloud costs. + Hey, {displayName}! {owner} doesn't seem to have any cloud + costs. {hasMultipleGroups && ( @@ -68,7 +67,7 @@ const CostInsightsHeaderAlerts = ({ owner, alerts, }: CostInsightsHeaderProps) => { - const profile = useApi(identityApiRef).getProfile(); + const displayName = useDisplayName(); const classes = useCostInsightsStyles(); return ( @@ -80,7 +79,7 @@ const CostInsightsHeaderAlerts = ({ You have {alerts} thing{alerts > 1 && 's'} to look into - Hey, {name(profile)}! We've identified{' '} + Hey, {displayName}! We've identified{' '} {alerts > 1 ? 'a few things ' : 'one thing '} {owner} should look into next. @@ -89,7 +88,7 @@ const CostInsightsHeaderAlerts = ({ }; const CostInsightsHeaderNoAlerts = ({ owner }: CostInsightsHeaderProps) => { - const profile = useApi(identityApiRef).getProfile(); + const displayName = useDisplayName(); const classes = useCostInsightsStyles(); return ( @@ -101,7 +100,7 @@ const CostInsightsHeaderNoAlerts = ({ owner }: CostInsightsHeaderProps) => { Your team is doing great - Hey, {name(profile)}! {owner} is doing well. No major + Hey, {displayName}! {owner} is doing well. No major changes this month. @@ -109,7 +108,7 @@ const CostInsightsHeaderNoAlerts = ({ owner }: CostInsightsHeaderProps) => { }; export const CostInsightsHeaderNoGroups = () => { - const profile = useApi(identityApiRef).getProfile(); + const displayName = useDisplayName(); const classes = useCostInsightsStyles(); return ( <> @@ -120,8 +119,7 @@ export const CostInsightsHeaderNoGroups = () => { Well this is awkward - Hey, {name(profile)}! It doesn't look like you belong to any - teams. + Hey, {displayName}! It doesn't look like you belong to any teams. ); diff --git a/plugins/cost-insights/src/hooks/useGroups.tsx b/plugins/cost-insights/src/hooks/useGroups.tsx index 6cbd0f7cd1..6d20f435b3 100644 --- a/plugins/cost-insights/src/hooks/useGroups.tsx +++ b/plugins/cost-insights/src/hooks/useGroups.tsx @@ -26,6 +26,10 @@ import { MapLoadingToProps, useLoading } from './useLoading'; import { Group, Maybe } from '../types'; import { DefaultLoadingAction } from '../utils/loading'; import { useApi, identityApiRef } from '@backstage/core-plugin-api'; +import { + ENTITY_DEFAULT_NAMESPACE, + parseEntityRef, +} from '@backstage/catalog-model'; type GroupsProviderLoadingProps = { dispatchLoadingGroups: (isLoading: boolean) => void; @@ -47,7 +51,7 @@ export const GroupsContext = React.createContext< >(undefined); export const GroupsProvider = ({ children }: PropsWithChildren<{}>) => { - const userId = useApi(identityApiRef).getUserId(); + const identityApi = useApi(identityApiRef); const client = useApi(costInsightsApiRef); const [error, setError] = useState>(null); const { dispatchLoadingGroups } = useLoading(mapLoadingToProps); @@ -59,6 +63,11 @@ export const GroupsProvider = ({ children }: PropsWithChildren<{}>) => { async function getUserGroups() { try { + const { userEntityRef } = await identityApi.getBackstageIdentity(); + const { name: userId } = parseEntityRef(userEntityRef, { + defaultKind: 'User', + defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + }); const g = await client.getUserGroups(userId); setGroups(g); } catch (e) { @@ -69,7 +78,7 @@ export const GroupsProvider = ({ children }: PropsWithChildren<{}>) => { } getUserGroups(); - }, [userId, client]); // eslint-disable-line react-hooks/exhaustive-deps + }, [client]); // eslint-disable-line react-hooks/exhaustive-deps if (error) { return {error.message}; diff --git a/plugins/fossa/src/api/FossaClient.test.ts b/plugins/fossa/src/api/FossaClient.test.ts index e723a4b00a..65b0fc1d53 100644 --- a/plugins/fossa/src/api/FossaClient.test.ts +++ b/plugins/fossa/src/api/FossaClient.test.ts @@ -25,8 +25,8 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api'; const server = setupServer(); const identityApi = { - async getIdToken() { - return Promise.resolve('fake-id-token'); + async getCredentials() { + return { token: 'fake-id-token' }; }, } as IdentityApi; diff --git a/plugins/fossa/src/api/FossaClient.ts b/plugins/fossa/src/api/FossaClient.ts index e2a21f3ef6..66b44de21e 100644 --- a/plugins/fossa/src/api/FossaClient.ts +++ b/plugins/fossa/src/api/FossaClient.ts @@ -57,7 +57,7 @@ export class FossaClient implements FossaApi { query: Record, ): Promise { const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`; - const idToken = await this.identityApi.getIdToken(); + const { token: idToken } = await this.identityApi.getCredentials(); const response = await fetch( `${apiUrl}/${path}?${new URLSearchParams(query).toString()}`, { diff --git a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx index 46ebc03ba3..d58fa91108 100644 --- a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx +++ b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx @@ -27,6 +27,10 @@ import { identityApiRef, } from '@backstage/core-plugin-api'; import { Progress, Link } from '@backstage/core-components'; +import { + ENTITY_DEFAULT_NAMESPACE, + parseEntityRef, +} from '@backstage/catalog-model'; export const IncidentActionsMenu = ({ incident, @@ -40,7 +44,6 @@ export const IncidentActionsMenu = ({ const ilertApi = useApi(ilertApiRef); const alertApi = useApi(alertApiRef); const identityApi = useApi(identityApiRef); - const userName = identityApi.getUserId(); const [anchorEl, setAnchorEl] = React.useState(null); const callback = onIncidentChanged || ((_: Incident): void => {}); const setProcessing = setIsLoading || ((_: boolean): void => {}); @@ -64,6 +67,12 @@ export const IncidentActionsMenu = ({ try { handleCloseMenu(); setProcessing(true); + + const { userEntityRef } = await identityApi.getBackstageIdentity(); + const { name: userName } = parseEntityRef(userEntityRef, { + defaultKind: 'User', + defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + }); const newIncident = await ilertApi.acceptIncident(incident, userName); alertApi.post({ message: 'Incident accepted.' }); @@ -79,6 +88,11 @@ export const IncidentActionsMenu = ({ try { handleCloseMenu(); setProcessing(true); + const { userEntityRef } = await identityApi.getBackstageIdentity(); + const { name: userName } = parseEntityRef(userEntityRef, { + defaultKind: 'User', + defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + }); const newIncident = await ilertApi.resolveIncident(incident, userName); alertApi.post({ message: 'Incident resolved.' }); diff --git a/plugins/ilert/src/components/Incident/IncidentNewModal.tsx b/plugins/ilert/src/components/Incident/IncidentNewModal.tsx index 006abec1e4..c8f63c6073 100644 --- a/plugins/ilert/src/components/Incident/IncidentNewModal.tsx +++ b/plugins/ilert/src/components/Incident/IncidentNewModal.tsx @@ -14,6 +14,10 @@ * limitations under the License. */ import React from 'react'; +import { + ENTITY_DEFAULT_NAMESPACE, + parseEntityRef, +} from '@backstage/catalog-model'; import { makeStyles } from '@material-ui/core/styles'; import Alert from '@material-ui/lab/Alert'; import Button from '@material-ui/core/Button'; @@ -80,7 +84,6 @@ export const IncidentNewModal = ({ const ilertApi = useApi(ilertApiRef); const alertApi = useApi(alertApiRef); const identityApi = useApi(identityApiRef); - const userName = identityApi.getUserId(); const source = window.location.toString(); const classes = useStyles(); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); @@ -102,6 +105,11 @@ export const IncidentNewModal = ({ setIsLoading(true); setTimeout(async () => { try { + const { userEntityRef } = await identityApi.getBackstageIdentity(); + const { name: userName } = parseEntityRef(userEntityRef, { + defaultKind: 'User', + defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + }); await ilertApi.createIncident({ integrationKey, summary, diff --git a/plugins/kafka/src/api/KafkaBackendClient.ts b/plugins/kafka/src/api/KafkaBackendClient.ts index aea5736aad..985e9a7ba4 100644 --- a/plugins/kafka/src/api/KafkaBackendClient.ts +++ b/plugins/kafka/src/api/KafkaBackendClient.ts @@ -31,7 +31,7 @@ export class KafkaBackendClient implements KafkaApi { private async internalGet(path: string): Promise { const url = `${await this.discoveryApi.getBaseUrl('kafka')}${path}`; - const idToken = await this.identityApi.getIdToken(); + const { token: idToken } = await this.identityApi.getCredentials(); const response = await fetch(url, { method: 'GET', headers: { diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index 4d44c021c4..209f5f950d 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -56,7 +56,7 @@ export class KubernetesBackendClient implements KubernetesApi { requestBody: KubernetesRequestBody, ): Promise { const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`; - const idToken = await this.identityApi.getIdToken(); + const { token: idToken } = await this.identityApi.getCredentials(); const response = await fetch(url, { method: 'POST', headers: { @@ -79,7 +79,7 @@ export class KubernetesBackendClient implements KubernetesApi { } async getClusters(): Promise<{ name: string; authProvider: string }[]> { - const idToken = await this.identityApi.getIdToken(); + const { token: idToken } = await this.identityApi.getCredentials(); const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/clusters`; const response = await fetch(url, { diff --git a/plugins/pagerduty/src/components/TriggerButton/index.test.tsx b/plugins/pagerduty/src/components/TriggerButton/index.test.tsx index 3776bcaf1c..409f11ec04 100644 --- a/plugins/pagerduty/src/components/TriggerButton/index.test.tsx +++ b/plugins/pagerduty/src/components/TriggerButton/index.test.tsx @@ -22,17 +22,9 @@ import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TriggerButton } from './'; import { ApiProvider } from '@backstage/core-app-api'; -import { - alertApiRef, - IdentityApi, - identityApiRef, -} from '@backstage/core-plugin-api'; +import { alertApiRef } from '@backstage/core-plugin-api'; describe('TriggerButton', () => { - const mockIdentityApi: Partial = { - getUserId: () => 'guest@example.com', - }; - const mockTriggerAlarmFn = jest.fn(); const mockPagerDutyApi = { triggerAlarm: mockTriggerAlarmFn, @@ -40,7 +32,6 @@ describe('TriggerButton', () => { const apis = TestApiRegistry.from( [alertApiRef, {}], - [identityApiRef, mockIdentityApi], [pagerDutyApiRef, mockPagerDutyApi], ); diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx index d6f378b4bf..c6874cc7f3 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -30,7 +30,11 @@ import { describe('TriggerDialog', () => { const mockIdentityApi: Partial = { - getUserId: () => 'guest@example.com', + getBackstageIdentity: async () => ({ + type: 'user', + userEntityRef: 'user:default/guest', + ownershipEntityRefs: [], + }), }; const mockTriggerAlarmFn = jest.fn(); @@ -89,7 +93,7 @@ describe('TriggerDialog', () => { entity!.metadata!.annotations!['pagerduty.com/integration-key'], source: window.location.toString(), description, - userName: 'guest@example.com', + userName: 'guest', }); }); }); diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx index 7e1a602bc8..ef0659becb 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx @@ -34,6 +34,10 @@ import { alertApiRef, identityApiRef, } from '@backstage/core-plugin-api'; +import { + ENTITY_DEFAULT_NAMESPACE, + parseEntityRef, +} from '@backstage/catalog-model'; type Props = { showDialog: boolean; @@ -49,18 +53,23 @@ export const TriggerDialog = ({ const { name, integrationKey } = usePagerdutyEntity(); const alertApi = useApi(alertApiRef); const identityApi = useApi(identityApiRef); - const userName = identityApi.getUserId(); const api = useApi(pagerDutyApiRef); const [description, setDescription] = useState(''); const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn( - async (descriptions: string) => + async (descriptions: string) => { + const { userEntityRef } = await identityApi.getBackstageIdentity(); + const { name: userName } = parseEntityRef(userEntityRef, { + defaultKind: 'User', + defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + }); await api.triggerAlarm({ integrationKey: integrationKey as string, source: window.location.toString(), description: descriptions, userName, - }), + }); + }, ); const descriptionChanged = ( @@ -73,7 +82,7 @@ export const TriggerDialog = ({ if (value) { (async () => { alertApi.post({ - message: `Alarm successfully triggered by ${userName}`, + message: `Alarm successfully triggered`, }); handleDialog(); @@ -83,7 +92,7 @@ export const TriggerDialog = ({ onIncidentCreated?.(); })(); } - }, [value, alertApi, handleDialog, userName, onIncidentCreated]); + }, [value, alertApi, handleDialog, onIncidentCreated]); if (error) { alertApi.post({ diff --git a/plugins/permission-react/src/apis/IdentityPermissionApi.ts b/plugins/permission-react/src/apis/IdentityPermissionApi.ts index 7d126c56cb..6e5d54f61d 100644 --- a/plugins/permission-react/src/apis/IdentityPermissionApi.ts +++ b/plugins/permission-react/src/apis/IdentityPermissionApi.ts @@ -45,9 +45,10 @@ export class IdentityPermissionApi implements PermissionApi { } async authorize(request: AuthorizeQuery): Promise { - const response = await this.permissionClient.authorize([request], { - token: await this.identityApi.getIdToken(), - }); + const response = await this.permissionClient.authorize( + [request], + await this.identityApi.getCredentials(), + ); return response[0]; } } diff --git a/plugins/rollbar/src/api/RollbarClient.ts b/plugins/rollbar/src/api/RollbarClient.ts index d3a88be46d..b87a6c8cc8 100644 --- a/plugins/rollbar/src/api/RollbarClient.ts +++ b/plugins/rollbar/src/api/RollbarClient.ts @@ -58,7 +58,7 @@ export class RollbarClient implements RollbarApi { private async get(path: string): Promise { const url = `${await this.discoveryApi.getBaseUrl('rollbar')}${path}`; - const idToken = await this.identityApi.getIdToken(); + const { token: idToken } = await this.identityApi.getCredentials(); const response = await fetch(url, { headers: idToken ? { Authorization: `Bearer ${idToken}` } : {}, }); diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index ef59d4382f..70ed803ef7 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -124,7 +124,7 @@ export class ScaffolderClient implements ScaffolderApi { ): Promise { const { namespace, kind, name } = templateName; - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const templatePath = [namespace, kind, name] .map(s => encodeURIComponent(s)) @@ -156,7 +156,7 @@ export class ScaffolderClient implements ScaffolderApi { templateName: string, values: Record, ): Promise { - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v2/tasks`; const response = await fetch(url, { method: 'POST', @@ -178,7 +178,7 @@ export class ScaffolderClient implements ScaffolderApi { } async getTask(taskId: string) { - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`; const response = await fetch(url, { @@ -295,7 +295,7 @@ export class ScaffolderClient implements ScaffolderApi { */ async listActions(): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const response = await fetch(`${baseUrl}/v2/actions`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }); diff --git a/plugins/search/src/apis.test.ts b/plugins/search/src/apis.test.ts index 5721812059..1c8f60d8e7 100644 --- a/plugins/search/src/apis.test.ts +++ b/plugins/search/src/apis.test.ts @@ -27,16 +27,13 @@ describe('apis', () => { const getBaseUrl = jest.fn().mockResolvedValue(baseUrl); const token = 'AUTHTOKEN'; - const withToken = jest.fn().mockResolvedValue(token); - const withoutToken = jest.fn().mockResolvedValue(undefined); - const createIdentityApiMock = (getIdToken: any) => ({ - getIdToken, - getUserId: jest.fn(), - getProfile: jest.fn(), + const withToken = jest.fn().mockResolvedValue({ token }); + const withoutToken = jest.fn().mockResolvedValue({ token: undefined }); + const createIdentityApiMock = (getCredentials: any) => ({ signOut: jest.fn(), getProfileInfo: jest.fn(), getBackstageIdentity: jest.fn(), - getCredentials: jest.fn(), + getCredentials, }); const client = new SearchClient({ diff --git a/plugins/search/src/apis.ts b/plugins/search/src/apis.ts index 4ce638fcca..008002deb1 100644 --- a/plugins/search/src/apis.ts +++ b/plugins/search/src/apis.ts @@ -44,7 +44,7 @@ export class SearchClient implements SearchApi { } async query(query: SearchQuery): Promise { - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const queryString = qs.stringify(query); const url = `${await this.discoveryApi.getBaseUrl( 'search/query', diff --git a/plugins/sentry/src/api/production-api.ts b/plugins/sentry/src/api/production-api.ts index 1fc6ece37f..5062f67380 100644 --- a/plugins/sentry/src/api/production-api.ts +++ b/plugins/sentry/src/api/production-api.ts @@ -55,7 +55,7 @@ export class ProductionSentryApi implements SentryApi { if (!this.identityApi) { return {}; } - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); return { headers: { authorization: `Bearer ${token}`, diff --git a/plugins/sonarqube/src/api/SonarQubeClient.test.ts b/plugins/sonarqube/src/api/SonarQubeClient.test.ts index a3a301d77a..b592953434 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.test.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.test.ts @@ -25,38 +25,16 @@ import { IdentityApi } from '@backstage/core-plugin-api'; const server = setupServer(); const identityApiAuthenticated: IdentityApi = { - getUserId() { - return 'jane-fonda'; - }, - getProfile() { - return { email: 'jane-fonda@spotify.com' }; - }, - async getIdToken() { - return Promise.resolve('fake-id-token'); - }, - async signOut() { - return Promise.resolve(); - }, + signOut: jest.fn(), getProfileInfo: jest.fn(), getBackstageIdentity: jest.fn(), - getCredentials: jest.fn(), + getCredentials: jest.fn().mockResolvedValue({ token: 'fake-id-token' }), }; const identityApiGuest: IdentityApi = { - getUserId() { - return 'guest'; - }, - getProfile() { - return {}; - }, - async getIdToken() { - return Promise.resolve(undefined); - }, - async signOut() { - return Promise.resolve(); - }, + signOut: jest.fn(), getProfileInfo: jest.fn(), getBackstageIdentity: jest.fn(), - getCredentials: jest.fn(), + getCredentials: jest.fn().mockResolvedValue({ token: undefined }), }; describe('SonarQubeClient', () => { diff --git a/plugins/sonarqube/src/api/SonarQubeClient.ts b/plugins/sonarqube/src/api/SonarQubeClient.ts index 85da65ecf3..39d0f11369 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.ts @@ -42,7 +42,7 @@ export class SonarQubeClient implements SonarQubeApi { path: string, query: { [key in string]: any }, ): Promise { - const idToken = await this.identityApi.getIdToken(); + const { token: idToken } = await this.identityApi.getCredentials(); const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sonarqube`; const response = await fetch( diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx index 181c2fda77..0f2d28927d 100644 --- a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx @@ -20,24 +20,15 @@ import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { splunkOnCallApiRef } from '../../api'; import { MOCK_TEAM, MOCK_INCIDENT } from '../../api/mocks'; -import { - alertApiRef, - IdentityApi, - identityApiRef, -} from '@backstage/core-plugin-api'; +import { alertApiRef } from '@backstage/core-plugin-api'; import { ApiProvider } from '@backstage/core-app-api'; -const mockIdentityApi: Partial = { - getUserId: () => 'test', -}; - const mockSplunkOnCallApi = { getIncidents: jest.fn(), getTeams: jest.fn(), }; const apis = TestApiRegistry.from( [alertApiRef, {}], - [identityApiRef, mockIdentityApi], [splunkOnCallApiRef, mockSplunkOnCallApi], ); diff --git a/plugins/tech-insights/src/api/TechInsightsClient.ts b/plugins/tech-insights/src/api/TechInsightsClient.ts index 692169e80e..5e29749854 100644 --- a/plugins/tech-insights/src/api/TechInsightsClient.ts +++ b/plugins/tech-insights/src/api/TechInsightsClient.ts @@ -59,7 +59,7 @@ export class TechInsightsClient implements TechInsightsApi { async getAllChecks(): Promise { const url = await this.discoveryApi.getBaseUrl('tech-insights'); - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const response = await fetch(`${url}/checks`, { headers: token ? { @@ -78,7 +78,7 @@ export class TechInsightsClient implements TechInsightsApi { checks?: Check[], ): Promise { const url = await this.discoveryApi.getBaseUrl('tech-insights'); - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const { namespace, kind, name } = entityParams; const checkIds = checks ? checks.map(check => check.id) : []; const requestBody = { checks: checkIds.length > 0 ? checkIds : undefined }; @@ -106,7 +106,7 @@ export class TechInsightsClient implements TechInsightsApi { checks?: Check[], ): Promise { const url = await this.discoveryApi.getBaseUrl('tech-insights'); - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const checkIds = checks ? checks.map(check => check.id) : []; const requestBody = { entities, diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index d1a421c69f..9853b99f2f 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -40,9 +40,6 @@ describe('TechDocsStorageClient', () => { } as Partial; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); const identityApi: jest.Mocked = { - getIdToken: jest.fn(), - getProfile: jest.fn(), - getUserId: jest.fn(), signOut: jest.fn(), getProfileInfo: jest.fn(), getBackstageIdentity: jest.fn(), @@ -51,6 +48,7 @@ describe('TechDocsStorageClient', () => { beforeEach(() => { jest.resetAllMocks(); + identityApi.getCredentials.mockResolvedValue({ token: undefined }); }); it('should return correct base url based on defined storage', async () => { @@ -122,7 +120,7 @@ describe('TechDocsStorageClient', () => { }, ); - identityApi.getIdToken.mockResolvedValue('token'); + identityApi.getCredentials.mockResolvedValue({ token: 'token' }); await storageApi.syncEntityDocs(mockEntity); diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index 3963637349..5cf33ac8d5 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -65,7 +65,7 @@ export class TechDocsClient implements TechDocsApi { const apiOrigin = await this.getApiOrigin(); const requestUrl = `${apiOrigin}/metadata/techdocs/${namespace}/${kind}/${name}`; - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const request = await fetch(`${requestUrl}`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, @@ -93,7 +93,7 @@ export class TechDocsClient implements TechDocsApi { const apiOrigin = await this.getApiOrigin(); const requestUrl = `${apiOrigin}/metadata/entity/${namespace}/${kind}/${name}`; - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const request = await fetch(`${requestUrl}`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, @@ -160,7 +160,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi { const storageUrl = await this.getStorageUrl(); const url = `${storageUrl}/${namespace}/${kind}/${name}/${path}`; - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const request = await fetch( `${url.endsWith('/') ? url : `${url}/`}index.html`, @@ -207,7 +207,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi { const apiOrigin = await this.getApiOrigin(); const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`; - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); return new Promise((resolve, reject) => { // Polyfill is used to add support for custom headers and auth diff --git a/plugins/todo/src/api/TodoClient.ts b/plugins/todo/src/api/TodoClient.ts index 7e4e4c8f79..4bfd5b77e5 100644 --- a/plugins/todo/src/api/TodoClient.ts +++ b/plugins/todo/src/api/TodoClient.ts @@ -46,7 +46,7 @@ export class TodoClient implements TodoApi { async listTodos(options: TodoListOptions): Promise { const { entity, offset, limit, orderBy, filters } = options; const baseUrl = await this.discoveryApi.getBaseUrl('todo'); - const token = await this.identityApi.getIdToken(); + const { token } = await this.identityApi.getCredentials(); const query = new URLSearchParams(); if (entity) {