diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/AppIdentityRegistryService.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/AppIdentityRegistryService.java index 3c0aba973d..0131e6dc91 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/AppIdentityRegistryService.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/AppIdentityRegistryService.java @@ -96,7 +96,11 @@ public Collection listAppIdentities(User loginUser) { if (loginUser.isSystemAdmin()) { return mds.getAppIdentityRegistry().appIds().values(); } else { - return mds.getAppIdentityRegistry().withoutSecret().appIds().values(); + return mds.getAppIdentityRegistry() + .withoutSecret() + .appIds() + .values().stream().filter(appIdentity -> !appIdentity.isSystemAdmin()) + .collect(toImmutableList()); } } diff --git a/webapp/src/dogma/features/app-identity/NewAppIdentity.tsx b/webapp/src/dogma/features/app-identity/NewAppIdentity.tsx index 452e115a44..5c2c9e0aa1 100644 --- a/webapp/src/dogma/features/app-identity/NewAppIdentity.tsx +++ b/webapp/src/dogma/features/app-identity/NewAppIdentity.tsx @@ -153,10 +153,7 @@ export const NewAppIdentity = () => { placeholder="my-app-id" {...register('appId', { pattern: APP_ID_PATTERN })} /> - - A unique identifier for the application. It must be registered with a project to access - repositories. - + App ID used to access project repositories. {errors.appId && ( The first/last character must be alphanumeric )} @@ -173,8 +170,7 @@ export const NewAppIdentity = () => { })} /> - An identifier extracted from the client certificate for mTLS authentication, e.g., Common - Name (CN) or SPIFFE ID in SAN. + Certificate identifier for mTLS authentication (e.g., CN or SPIFFE ID). {errors.certificateId && Certificate ID is required} diff --git a/webapp/src/pages/app/settings/app-identities/index.tsx b/webapp/src/pages/app/settings/app-identities/index.tsx index 882c5b6d66..f6667b6531 100644 --- a/webapp/src/pages/app/settings/app-identities/index.tsx +++ b/webapp/src/pages/app/settings/app-identities/index.tsx @@ -14,19 +14,13 @@ import { ActivateAppIdentity } from 'dogma/features/app-identity/ActivateAppIden import { DeleteAppIdentity } from 'dogma/features/app-identity/DeleteAppIdentity'; import { Deferred } from 'dogma/common/components/Deferred'; import SettingView from 'dogma/features/settings/SettingView'; +import { useAppSelector } from 'dogma/hooks'; const AppIdentityPage = () => { + const systemAdmin = useAppSelector((state) => state.auth.user?.systemAdmin ?? false); const columnHelper = createColumnHelper(); const columns = useMemo( () => [ - columnHelper.accessor((row: AppIdentityDto) => row.type, { - cell: (info) => ( - - {info.getValue() === 'TOKEN' ? 'Token' : 'Certificate'} - - ), - header: 'Type', - }), columnHelper.accessor((row: AppIdentityDto) => row.appId, { cell: (info) => { const identity = info.row.original; @@ -47,10 +41,22 @@ const AppIdentityPage = () => { }, header: 'Application ID', }), - columnHelper.accessor((row: AppIdentityDto) => row.systemAdmin, { - cell: (info) => , - header: 'Level', + columnHelper.accessor((row: AppIdentityDto) => row.type, { + cell: (info) => ( + + {info.getValue() === 'TOKEN' ? 'Token' : 'Certificate'} + + ), + header: 'Type', }), + ...(systemAdmin + ? [ + columnHelper.accessor((row: AppIdentityDto) => row.systemAdmin, { + cell: (info) => , + header: 'Level', + }), + ] + : []), columnHelper.accessor((row: AppIdentityDto) => row.creation.user, { cell: (info) => {info.getValue()}, header: 'Created By', @@ -79,7 +85,7 @@ const AppIdentityPage = () => { enableSorting: false, }), ], - [columnHelper], + [columnHelper, systemAdmin], ); const { data, error, isLoading } = useGetAppIdentitiesQuery(); return ( diff --git a/webapp/tests/pages/app/settings/app-identities/index.test.tsx b/webapp/tests/pages/app/settings/app-identities/index.test.tsx new file mode 100644 index 0000000000..5c9c6e0bb7 --- /dev/null +++ b/webapp/tests/pages/app/settings/app-identities/index.test.tsx @@ -0,0 +1,92 @@ +import '@testing-library/jest-dom'; +import { renderWithProviders } from 'dogma/util/test-utils'; +import AppIdentityPage from 'pages/app/settings/app-identities'; +import { AppIdentityDto } from 'dogma/features/app-identity/AppIdentity'; +import { useGetAppIdentitiesQuery } from 'dogma/features/api/apiSlice'; + +jest.mock('dogma/features/api/apiSlice', () => ({ + ...jest.requireActual('dogma/features/api/apiSlice'), + useGetAppIdentitiesQuery: jest.fn(), +})); + +jest.mock('next/router', () => ({ + useRouter: () => ({ asPath: '/app/settings/app-identities' }), +})); + +const mockIdentities: AppIdentityDto[] = [ + { + appId: 'app-token-1', + type: 'TOKEN', + systemAdmin: false, + allowGuestAccess: false, + creation: { user: 'user@example.com', timestamp: '2024-01-01T00:00:00Z' }, + }, + { + appId: 'app-admin-1', + type: 'TOKEN', + systemAdmin: true, + allowGuestAccess: false, + creation: { user: 'admin@example.com', timestamp: '2024-01-02T00:00:00Z' }, + }, +]; + +const baseAuthState = { + isInAnonymousMode: false, + csrfToken: null, + isLoading: false, + user: { + login: 'user', + name: 'Test User', + email: 'user@example.com', + roles: [], + systemAdmin: false, + }, +}; + +describe('AppIdentityPage', () => { + beforeEach(() => { + (useGetAppIdentitiesQuery as jest.Mock).mockReturnValue({ + data: mockIdentities, + error: undefined, + isLoading: false, + }); + }); + + it('hides the Level column for non-system-admin users', () => { + const { queryByText } = renderWithProviders(, { + preloadedState: { auth: baseAuthState }, + }); + + expect(queryByText('Level')).not.toBeInTheDocument(); + }); + + it('shows the Level column for system-admin users', () => { + const { getByText } = renderWithProviders(, { + preloadedState: { + auth: { ...baseAuthState, user: { ...baseAuthState.user, systemAdmin: true } }, + }, + }); + + expect(getByText('Level')).toBeInTheDocument(); + }); + + it('renders System Admin and User badges in the Level column for system-admin users', () => { + const { getByText } = renderWithProviders(, { + preloadedState: { + auth: { ...baseAuthState, user: { ...baseAuthState.user, systemAdmin: true } }, + }, + }); + + expect(getByText('System Admin')).toBeInTheDocument(); + expect(getByText('User')).toBeInTheDocument(); + }); + + it('displays app identities in the table', () => { + const { getByText } = renderWithProviders(, { + preloadedState: { auth: baseAuthState }, + }); + + expect(getByText('app-token-1')).toBeInTheDocument(); + expect(getByText('app-admin-1')).toBeInTheDocument(); + }); +});