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
19 changes: 16 additions & 3 deletions frontend/src/lib/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,19 @@ export function createDemoClient(): ParseableClient {
const missing = names.filter((name) => !Object.hasOwn(roles, name));
if (missing.length) fail(`Roles do not exist: ${missing.join(', ')}`);
}
// Demo roles can only hold admin, editor, reader, writer or ingestor, so
// the built-in `super-admin` is the only protected role.
function rejectProtectedRoles(names: string[]) {
if (names.includes('super-admin')) fail('Role is protected');
}
// Like the backend, role additions report protected roles before unknown
// ones, and API keys and user creation the other way round. `super-admin`
// exists on the backend but not in the demo's role map.
function assignableRoles(names: string[], { protectedFirst = false } = {}) {
if (protectedFirst) rejectProtectedRoles(names);
knownRoles(names.filter((name) => name !== 'super-admin'));
rejectProtectedRoles(names);
}
function named(name: string, kind: 'user' | 'role') {
const error = validateName(name, kind);
if (error) fail(error);
Expand Down Expand Up @@ -613,7 +626,7 @@ export function createDemoClient(): ParseableClient {
await ready();
if (username === 'admin') fail('Cannot call this API for root admin user');
named(username, 'user');
knownRoles(assigned);
assignableRoles(assigned);
if (!assigned.length) fail('User cannot be created without a role');
if (
users.some(
Expand Down Expand Up @@ -641,7 +654,7 @@ export function createDemoClient(): ParseableClient {
async addUserRoles(id, assigned) {
await ready();
const user = findUser(id, true);
knownRoles(assigned);
assignableRoles(assigned, { protectedFirst: true });
if (user.oidc && !user.oidc.legacy)
user.oidc.manualRoles = [...new Set([...(user.oidc.manualRoles ?? []), ...assigned])];
else user.assigned = [...new Set([...user.assigned, ...assigned])];
Expand Down Expand Up @@ -678,7 +691,7 @@ export function createDemoClient(): ParseableClient {
},
async createApiKey(keyName, assigned, ttlSeconds) {
await ready();
knownRoles(assigned);
assignableRoles(assigned);
if (
ttlSeconds != null &&
(!Number.isInteger(ttlSeconds) || ttlSeconds < 1800 || ttlSeconds > 7776000)
Expand Down
73 changes: 73 additions & 0 deletions frontend/src/lib/demoTeam.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,79 @@ describe('per-client Team demo', () => {
}
expect(await client.listUsers()).toEqual(users);
});
describe.each([
{ assigned: ['super-admin'] },
{ assigned: ['analysts', 'super-admin'] },
{ assigned: ['super-admin', 'analysts'] },
])('protected role grants: $assigned', ({ assigned }) => {
it('rejects API-key creation without changing the key inventory', async () => {
const client = createDemoClient();
const keys = await client.listApiKeys();
await expect(client.createApiKey('protected-key', assigned)).rejects.toMatchObject({
status: 400,
message: 'Role is protected',
});
expect(await client.listApiKeys()).toEqual(keys);
});
it('rejects user creation without changing users', async () => {
const client = createDemoClient();
const users = await client.listUsers();
await expect(client.createUser('protected-user', assigned)).rejects.toMatchObject({
status: 400,
message: 'Role is protected',
});
expect(await client.listUsers()).toEqual(users);
});
it.each(['analyst', 'https://issuer.example/sso-user'])(
'rejects role additions to %s without changing role sources',
async (id) => {
const client = createDemoClient();
const sources = await client.userRoleSources(id);
await expect(client.addUserRoles(id, assigned)).rejects.toMatchObject({
status: 400,
message: 'Role is protected',
});
expect(await client.userRoleSources(id)).toEqual(sources);
},
);
});
it.each(['constructor', 'toString', '__proto__'])(
'rejects the inherited property name %s as an unknown role',
async (name) => {
const client = createDemoClient();
const unknown = { status: 400, message: `Roles do not exist: ${name}` };
await expect(client.createApiKey('inherited-key', [name])).rejects.toMatchObject(unknown);
await expect(client.createUser('inherited-user', [name])).rejects.toMatchObject(unknown);
await expect(client.addUserRoles('analyst', [name])).rejects.toMatchObject(unknown);
},
);
// Like the backend: API keys and user creation report unknown roles first,
// while role additions report protected roles first.
it('orders unknown and protected role errors like the backend', async () => {
const client = createDemoClient();
const assigned = ['nope', 'super-admin'];
const unknown = { status: 400, message: 'Roles do not exist: nope' };
await expect(client.createApiKey('mixed-key', assigned)).rejects.toMatchObject(unknown);
await expect(client.createUser('mixed-user', assigned)).rejects.toMatchObject(unknown);
await expect(client.addUserRoles('analyst', assigned)).rejects.toMatchObject({
status: 400,
message: 'Role is protected',
});
});
it('allows ordinary admin roles for API keys and user grants', async () => {
const client = createDemoClient();
expect((await client.createApiKey('admin-key', ['administrators'])).roles).toEqual([
'administrators',
]);
await client.createUser('admin-user', ['administrators']);
await client.addUserRoles('analyst', ['administrators']);
for (const id of ['admin-user', 'analyst']) {
expect((await client.userRoleSources(id)).roles.administrators).toEqual({
actions: [{ privilege: 'admin' }],
roleType: 'user',
});
}
});
it('preserves provider overlap, removes manual grants and reconciles the fallback', async () => {
const client = createDemoClient();
const oauth = (await client.listUsers()).find((user) => user.method === 'oauth')!;
Expand Down
Loading