Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ public Collection<AppIdentity> 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());
}
}

Expand Down
8 changes: 2 additions & 6 deletions webapp/src/dogma/features/app-identity/NewAppIdentity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,7 @@ export const NewAppIdentity = () => {
placeholder="my-app-id"
{...register('appId', { pattern: APP_ID_PATTERN })}
/>
<FormHelperText pl={1}>
A unique identifier for the application. It must be registered with a project to access
repositories.
</FormHelperText>
<FormHelperText pl={1}>App ID used to access project repositories.</FormHelperText>
{errors.appId && (
<FormErrorMessage>The first/last character must be alphanumeric</FormErrorMessage>
)}
Expand All @@ -173,8 +170,7 @@ export const NewAppIdentity = () => {
})}
/>
<FormHelperText pl={1}>
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).
</FormHelperText>
{errors.certificateId && <FormErrorMessage>Certificate ID is required</FormErrorMessage>}
</FormControl>
Expand Down
30 changes: 18 additions & 12 deletions webapp/src/pages/app/settings/app-identities/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppIdentityDto>();
const columns = useMemo(
() => [
columnHelper.accessor((row: AppIdentityDto) => row.type, {
cell: (info) => (
<Badge colorScheme={info.getValue() === 'TOKEN' ? 'purple' : 'green'}>
{info.getValue() === 'TOKEN' ? 'Token' : 'Certificate'}
</Badge>
),
header: 'Type',
}),
columnHelper.accessor((row: AppIdentityDto) => row.appId, {
cell: (info) => {
const identity = info.row.original;
Expand All @@ -47,10 +41,22 @@ const AppIdentityPage = () => {
},
header: 'Application ID',
}),
columnHelper.accessor((row: AppIdentityDto) => row.systemAdmin, {
cell: (info) => <UserRole role={info.getValue() ? 'System Admin' : 'User'} />,
header: 'Level',
columnHelper.accessor((row: AppIdentityDto) => row.type, {
cell: (info) => (
<Badge colorScheme={info.getValue() === 'TOKEN' ? 'purple' : 'green'}>
{info.getValue() === 'TOKEN' ? 'Token' : 'Certificate'}
</Badge>
),
header: 'Type',
}),
...(systemAdmin
? [
columnHelper.accessor((row: AppIdentityDto) => row.systemAdmin, {
cell: (info) => <UserRole role={info.getValue() ? 'System Admin' : 'User'} />,
header: 'Level',
}),
]
: []),
columnHelper.accessor((row: AppIdentityDto) => row.creation.user, {
cell: (info) => <Text>{info.getValue()}</Text>,
header: 'Created By',
Expand Down Expand Up @@ -79,7 +85,7 @@ const AppIdentityPage = () => {
enableSorting: false,
}),
],
[columnHelper],
[columnHelper, systemAdmin],
);
const { data, error, isLoading } = useGetAppIdentitiesQuery();
return (
Expand Down
92 changes: 92 additions & 0 deletions webapp/tests/pages/app/settings/app-identities/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<AppIdentityPage />, {
preloadedState: { auth: baseAuthState },
});

expect(queryByText('Level')).not.toBeInTheDocument();
});

it('shows the Level column for system-admin users', () => {
const { getByText } = renderWithProviders(<AppIdentityPage />, {
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(<AppIdentityPage />, {
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(<AppIdentityPage />, {
preloadedState: { auth: baseAuthState },
});

expect(getByText('app-token-1')).toBeInTheDocument();
expect(getByText('app-admin-1')).toBeInTheDocument();
});
});
Loading