feat(auth-backend): add who-am-i action to actions registry (#33046)
Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Added `who-am-i` action to the auth backend actions registry. Returns the catalog entity and user info for the currently authenticated user.
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2025 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 { mockCredentials, mockServices } from '@backstage/backend-test-utils';
|
||||
import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
|
||||
import { createWhoAmIAction } from './createWhoAmIAction';
|
||||
|
||||
const mockUserEntity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'mock',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
profile: {
|
||||
displayName: 'Mock User',
|
||||
email: 'mock@example.com',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('createWhoAmIAction', () => {
|
||||
it('should return the user entity and user info for authenticated user credentials', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
|
||||
createWhoAmIAction({
|
||||
auth: mockServices.auth(),
|
||||
catalog: catalogServiceMock({ entities: [mockUserEntity] }),
|
||||
userInfo: mockServices.userInfo(),
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
});
|
||||
|
||||
const result = await mockActionsRegistry.invoke({
|
||||
id: 'test:who-am-i',
|
||||
input: {},
|
||||
credentials: mockCredentials.user(),
|
||||
});
|
||||
|
||||
expect(result.output).toEqual({
|
||||
entity: mockUserEntity,
|
||||
userInfo: {
|
||||
userEntityRef: 'user:default/mock',
|
||||
ownershipEntityRefs: ['user:default/mock'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when called with service credentials', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
|
||||
createWhoAmIAction({
|
||||
auth: mockServices.auth(),
|
||||
catalog: catalogServiceMock({ entities: [mockUserEntity] }),
|
||||
userInfo: mockServices.userInfo(),
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockActionsRegistry.invoke({
|
||||
id: 'test:who-am-i',
|
||||
input: {},
|
||||
credentials: mockCredentials.service(),
|
||||
}),
|
||||
).rejects.toThrow('This action requires user credentials');
|
||||
});
|
||||
|
||||
it('should throw when the user entity is not found in the catalog', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
|
||||
createWhoAmIAction({
|
||||
auth: mockServices.auth(),
|
||||
catalog: catalogServiceMock(),
|
||||
userInfo: mockServices.userInfo(),
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockActionsRegistry.invoke({
|
||||
id: 'test:who-am-i',
|
||||
input: {},
|
||||
credentials: mockCredentials.user(),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'User entity not found in the catalog for "user:default/mock"',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2025 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 { AuthService, UserInfoService } from '@backstage/backend-plugin-api';
|
||||
import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { NotAllowedError, NotFoundError } from '@backstage/errors';
|
||||
import { CatalogService } from '@backstage/plugin-catalog-node';
|
||||
|
||||
export const createWhoAmIAction = ({
|
||||
auth,
|
||||
catalog,
|
||||
userInfo,
|
||||
actionsRegistry,
|
||||
}: {
|
||||
auth: AuthService;
|
||||
catalog: CatalogService;
|
||||
userInfo: UserInfoService;
|
||||
actionsRegistry: ActionsRegistryService;
|
||||
}) => {
|
||||
actionsRegistry.register({
|
||||
name: 'who-am-i',
|
||||
title: 'Who Am I',
|
||||
attributes: {
|
||||
destructive: false,
|
||||
readOnly: true,
|
||||
idempotent: true,
|
||||
},
|
||||
description:
|
||||
'Returns the catalog entity and user info for the currently authenticated user. This action requires user credentials and cannot be used with service or unauthenticated credentials.',
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z =>
|
||||
z.object({
|
||||
entity: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.describe('The full catalog entity for the authenticated user'),
|
||||
userInfo: z
|
||||
.object({
|
||||
userEntityRef: z
|
||||
.string()
|
||||
.describe(
|
||||
'The entity ref of the user, e.g. user:default/jane.doe',
|
||||
),
|
||||
ownershipEntityRefs: z
|
||||
.array(z.string())
|
||||
.describe('Entity refs that the user claims ownership through'),
|
||||
})
|
||||
.describe(
|
||||
'User identity information extracted from the authentication token',
|
||||
),
|
||||
}),
|
||||
},
|
||||
action: async ({ credentials }) => {
|
||||
if (!auth.isPrincipal(credentials, 'user')) {
|
||||
throw new NotAllowedError('This action requires user credentials');
|
||||
}
|
||||
|
||||
const { userEntityRef } = credentials.principal;
|
||||
|
||||
const [entity, info] = await Promise.all([
|
||||
catalog.getEntityByRef(userEntityRef, { credentials }),
|
||||
userInfo.getUserInfo(credentials),
|
||||
]);
|
||||
|
||||
if (!entity) {
|
||||
throw new NotFoundError(
|
||||
`User entity not found in the catalog for "${userEntityRef}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
output: {
|
||||
entity,
|
||||
userInfo: info,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2025 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 { AuthService, UserInfoService } from '@backstage/backend-plugin-api';
|
||||
import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { CatalogService } from '@backstage/plugin-catalog-node';
|
||||
import { createWhoAmIAction } from './createWhoAmIAction';
|
||||
|
||||
export const createAuthActions = (options: {
|
||||
auth: AuthService;
|
||||
actionsRegistry: ActionsRegistryService;
|
||||
catalog: CatalogService;
|
||||
userInfo: UserInfoService;
|
||||
}) => {
|
||||
createWhoAmIAction(options);
|
||||
};
|
||||
@@ -24,7 +24,9 @@ import {
|
||||
AuthProviderFactory,
|
||||
authProvidersExtensionPoint,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';
|
||||
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
|
||||
import { createAuthActions } from './actions';
|
||||
import { createRouter } from './service/router';
|
||||
import { OfflineAccessService } from './service/OfflineAccessService';
|
||||
|
||||
@@ -70,6 +72,8 @@ export const authPlugin = createBackendPlugin({
|
||||
httpAuth: coreServices.httpAuth,
|
||||
lifecycle: coreServices.lifecycle,
|
||||
catalog: catalogServiceRef,
|
||||
actionsRegistry: actionsRegistryServiceRef,
|
||||
userInfo: coreServices.userInfo,
|
||||
},
|
||||
async init({
|
||||
httpRouter,
|
||||
@@ -81,6 +85,8 @@ export const authPlugin = createBackendPlugin({
|
||||
httpAuth,
|
||||
lifecycle,
|
||||
catalog,
|
||||
actionsRegistry,
|
||||
userInfo,
|
||||
}) {
|
||||
const refreshTokensEnabled = config.getOptionalBoolean(
|
||||
'auth.experimentalRefreshToken.enabled',
|
||||
@@ -112,6 +118,8 @@ export const authPlugin = createBackendPlugin({
|
||||
allow: 'unauthenticated',
|
||||
});
|
||||
httpRouter.use(router);
|
||||
|
||||
createAuthActions({ auth, catalog, userInfo, actionsRegistry });
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user