From 2916a83b9cd1a218b1821c92544ffe8cca8b3688 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 4 Jan 2022 22:10:43 +0100 Subject: [PATCH] catalog-react: deprecate loadIdentityOwnerRefs and simplify implementation Signed-off-by: Patrik Oldsberg --- .changeset/modern-buttons-draw.md | 5 + plugins/catalog-react/api-report.md | 2 +- .../src/hooks/useEntityOwnership.test.tsx | 120 +++--------------- .../src/hooks/useEntityOwnership.ts | 52 ++------ 4 files changed, 31 insertions(+), 148 deletions(-) create mode 100644 .changeset/modern-buttons-draw.md 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/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/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(() => {