mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(core,schemas): update roles table schemas, add type column (#4378)
refactor(core,schemas): update roles table schemas, add type col and fix UTs refactor(test,core,schemas): add role type constraint to DB level to keep the data source clean
This commit is contained in:
parent
d1b92e99aa
commit
5d78c7271b
42 changed files with 325 additions and 136 deletions
9
.changeset/violet-mugs-tickle.md
Normal file
9
.changeset/violet-mugs-tickle.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
"@logto/schemas": minor
|
||||
---
|
||||
|
||||
Add `type` field to `roles` schema.
|
||||
|
||||
`type` can be either 'User' or 'MachineToMachine' in our case, this change distinguish between the two types of roles.
|
||||
Roles with type 'MachineToMachine' are not allowed to be assigned to users and 'User' roles can not be assigned to machine-to-machine apps.
|
||||
It's worth noting that we do not differentiate by `scope` (or `permission` in Admin Console), so a scope can be assigned to both the 'User' role and the 'MachineToMachine' role simultaneously.
|
|
@ -113,6 +113,11 @@ const queryDatabaseManifest = async (database) => {
|
|||
constraints: omitArray(
|
||||
constraints,
|
||||
'oid',
|
||||
/**
|
||||
* See https://www.postgresql.org/docs/current/catalog-pg-constraint.html, better to use `pg_get_constraintdef()`
|
||||
* to extract the definition of check constraint, so this can be omitted since conbin changes with the status of the computing resources.
|
||||
*/
|
||||
'conbin',
|
||||
'connamespace',
|
||||
'conrelid',
|
||||
'contypid',
|
||||
|
|
|
@ -8,7 +8,7 @@ import type {
|
|||
Scope,
|
||||
UsersRole,
|
||||
} from '@logto/schemas';
|
||||
import { ApplicationType } from '@logto/schemas';
|
||||
import { RoleType, ApplicationType } from '@logto/schemas';
|
||||
|
||||
export * from './connector.js';
|
||||
export * from './sign-in-experience.js';
|
||||
|
@ -58,18 +58,20 @@ export const mockScopeWithResource = {
|
|||
resource: mockResource,
|
||||
};
|
||||
|
||||
export const mockRole: Role = {
|
||||
export const mockAdminUserRole: Role = {
|
||||
tenantId: 'fake_tenant',
|
||||
id: 'role_id',
|
||||
name: 'admin',
|
||||
description: 'admin',
|
||||
type: RoleType.User,
|
||||
};
|
||||
|
||||
export const mockRole2: Role = {
|
||||
export const mockAdminUserRole2: Role = {
|
||||
tenantId: 'fake_tenant',
|
||||
id: 'role_id2',
|
||||
name: 'admin2',
|
||||
description: 'admin2',
|
||||
type: RoleType.User,
|
||||
};
|
||||
|
||||
export const mockAdminConsoleData: AdminConsoleData = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { UsersPasswordEncryptionMethod } from '@logto/schemas';
|
||||
|
||||
import { mockResource, mockRole, mockScope } from '#src/__mocks__/index.js';
|
||||
import { mockResource, mockAdminUserRole, mockScope } from '#src/__mocks__/index.js';
|
||||
import { mockUser } from '#src/__mocks__/user.js';
|
||||
import { MockQueries } from '#src/test-utils/tenant.js';
|
||||
|
||||
|
@ -11,7 +11,7 @@ const { encryptUserPassword, createUserLibrary } = await import('./user.js');
|
|||
const hasUserWithId = jest.fn();
|
||||
const queries = new MockQueries({
|
||||
users: { hasUserWithId },
|
||||
roles: { findRolesByRoleIds: async () => [mockRole] },
|
||||
roles: { findRolesByRoleIds: async () => [mockAdminUserRole] },
|
||||
scopes: { findScopesByIdsAndResourceIndicator: async () => [mockScope] },
|
||||
usersRoles: { findUsersRolesByUserId: async () => [] },
|
||||
rolesScopes: { findRolesScopesByRoleIds: async () => [] },
|
||||
|
@ -81,6 +81,6 @@ describe('findUserRoles()', () => {
|
|||
const { findUserRoles } = createUserLibrary(queries);
|
||||
|
||||
it('returns user roles', async () => {
|
||||
await expect(findUserRoles(mockUser.id)).resolves.toEqual([mockRole]);
|
||||
await expect(findUserRoles(mockUser.id)).resolves.toEqual([mockAdminUserRole]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import type { SchemaLike } from '@logto/schemas';
|
||||
import type { Middleware } from 'koa';
|
||||
import { SlonikError, NotFoundError, InvalidInputError } from 'slonik';
|
||||
import {
|
||||
SlonikError,
|
||||
NotFoundError,
|
||||
InvalidInputError,
|
||||
CheckIntegrityConstraintViolationError,
|
||||
} from 'slonik';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { DeletionError, InsertionError, UpdateError } from '#src/errors/SlonikError/index.js';
|
||||
|
@ -21,6 +26,13 @@ export default function koaSlonikErrorHandler<StateT, ContextT>(): Middleware<St
|
|||
});
|
||||
}
|
||||
|
||||
if (error instanceof CheckIntegrityConstraintViolationError) {
|
||||
throw new RequestError({
|
||||
code: 'entity.db_constraint_violated',
|
||||
status: 422,
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof InsertionError) {
|
||||
throw new RequestError({
|
||||
code: 'entity.create_failed',
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Roles } from '@logto/schemas';
|
|||
import { convertToIdentifiers, convertToPrimitiveOrSql, excludeAutoSetFields } from '@logto/shared';
|
||||
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
||||
|
||||
import { mockRole } from '#src/__mocks__/index.js';
|
||||
import { mockAdminUserRole } from '#src/__mocks__/index.js';
|
||||
import { DeletionError } from '#src/errors/SlonikError/index.js';
|
||||
import type { QueryType } from '#src/utils/test-utils.js';
|
||||
import { expectSqlAssert } from '#src/utils/test-utils.js';
|
||||
|
@ -33,7 +33,7 @@ describe('roles query', () => {
|
|||
const { table, fields } = convertToIdentifiers(Roles);
|
||||
|
||||
it('findRolesByRoleIds', async () => {
|
||||
const roleIds = [mockRole.id];
|
||||
const roleIds = [mockAdminUserRole.id];
|
||||
const expectSql = sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
|
@ -44,45 +44,47 @@ describe('roles query', () => {
|
|||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([roleIds.join(', ')]);
|
||||
|
||||
return createMockQueryResult([mockRole]);
|
||||
return createMockQueryResult([mockAdminUserRole]);
|
||||
});
|
||||
|
||||
await expect(findRolesByRoleIds(roleIds)).resolves.toEqual([mockRole]);
|
||||
await expect(findRolesByRoleIds(roleIds)).resolves.toEqual([mockAdminUserRole]);
|
||||
});
|
||||
|
||||
it('findRoleByRoleName', async () => {
|
||||
const expectSql = sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.name} = ${mockRole.name}
|
||||
where ${fields.name} = ${mockAdminUserRole.name}
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([mockRole.name]);
|
||||
expect(values).toEqual([mockAdminUserRole.name]);
|
||||
|
||||
return createMockQueryResult([mockRole]);
|
||||
return createMockQueryResult([mockAdminUserRole]);
|
||||
});
|
||||
|
||||
await expect(findRoleByRoleName(mockRole.name)).resolves.toEqual(mockRole);
|
||||
await expect(findRoleByRoleName(mockAdminUserRole.name)).resolves.toEqual(mockAdminUserRole);
|
||||
});
|
||||
|
||||
it('findRoleByRoleName with excludeRoleId', async () => {
|
||||
const expectSql = sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.name} = ${mockRole.name}
|
||||
and ${fields.id}<>${mockRole.id}
|
||||
where ${fields.name} = ${mockAdminUserRole.name}
|
||||
and ${fields.id}<>${mockAdminUserRole.id}
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([mockRole.name, mockRole.id]);
|
||||
expect(values).toEqual([mockAdminUserRole.name, mockAdminUserRole.id]);
|
||||
|
||||
return createMockQueryResult([mockRole]);
|
||||
return createMockQueryResult([mockAdminUserRole]);
|
||||
});
|
||||
|
||||
await expect(findRoleByRoleName(mockRole.name, mockRole.id)).resolves.toEqual(mockRole);
|
||||
await expect(findRoleByRoleName(mockAdminUserRole.name, mockAdminUserRole.id)).resolves.toEqual(
|
||||
mockAdminUserRole
|
||||
);
|
||||
});
|
||||
|
||||
it('findRolesByRoleNames', async () => {
|
||||
|
@ -98,10 +100,10 @@ describe('roles query', () => {
|
|||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([roleNames.join(', ')]);
|
||||
|
||||
return createMockQueryResult([mockRole]);
|
||||
return createMockQueryResult([mockAdminUserRole]);
|
||||
});
|
||||
|
||||
await expect(findRolesByRoleNames(roleNames)).resolves.toEqual([mockRole]);
|
||||
await expect(findRolesByRoleNames(roleNames)).resolves.toEqual([mockAdminUserRole]);
|
||||
});
|
||||
|
||||
it('insertRoles', async () => {
|
||||
|
@ -113,19 +115,23 @@ describe('roles query', () => {
|
|||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
|
||||
expect(values).toEqual([mockRole.id, mockRole.name, mockRole.description]);
|
||||
expect(values).toEqual([
|
||||
mockAdminUserRole.id,
|
||||
mockAdminUserRole.name,
|
||||
mockAdminUserRole.description,
|
||||
]);
|
||||
|
||||
return createMockQueryResult([mockRole]);
|
||||
return createMockQueryResult([mockAdminUserRole]);
|
||||
});
|
||||
|
||||
await insertRoles([mockRole]);
|
||||
await insertRoles([mockAdminUserRole]);
|
||||
});
|
||||
|
||||
it('insertRole', async () => {
|
||||
const keys = excludeAutoSetFields(Roles.fieldKeys);
|
||||
|
||||
const expectSql = `
|
||||
insert into "roles" ("id", "name", "description")
|
||||
insert into "roles" ("id", "name", "description", "type")
|
||||
values (${keys.map((_, index) => `$${index + 1}`).join(', ')})
|
||||
returning *
|
||||
`;
|
||||
|
@ -134,12 +140,12 @@ describe('roles query', () => {
|
|||
const rowData = { id: 'foo' };
|
||||
expectSqlAssert(sql, expectSql);
|
||||
|
||||
expect(values).toEqual(keys.map((k) => convertToPrimitiveOrSql(k, mockRole[k])));
|
||||
expect(values).toEqual(keys.map((k) => convertToPrimitiveOrSql(k, mockAdminUserRole[k])));
|
||||
|
||||
return createMockQueryResult([rowData]);
|
||||
});
|
||||
|
||||
await insertRole(mockRole);
|
||||
await insertRole(mockAdminUserRole);
|
||||
});
|
||||
|
||||
it('findRoleById', async () => {
|
||||
|
@ -151,16 +157,16 @@ describe('roles query', () => {
|
|||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([mockRole.id]);
|
||||
expect(values).toEqual([mockAdminUserRole.id]);
|
||||
|
||||
return createMockQueryResult([mockRole]);
|
||||
return createMockQueryResult([mockAdminUserRole]);
|
||||
});
|
||||
|
||||
await findRoleById(mockRole.id);
|
||||
await findRoleById(mockAdminUserRole.id);
|
||||
});
|
||||
|
||||
it('updateRoleById', async () => {
|
||||
const { id, description } = mockRole;
|
||||
const { id, description } = mockAdminUserRole;
|
||||
|
||||
const expectSql = sql`
|
||||
update ${table}
|
||||
|
@ -187,16 +193,16 @@ describe('roles query', () => {
|
|||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([mockRole.id]);
|
||||
expect(values).toEqual([mockAdminUserRole.id]);
|
||||
|
||||
return createMockQueryResult([mockRole]);
|
||||
return createMockQueryResult([mockAdminUserRole]);
|
||||
});
|
||||
|
||||
await deleteRoleById(mockRole.id);
|
||||
await deleteRoleById(mockAdminUserRole.id);
|
||||
});
|
||||
|
||||
it('deleteRoleById throw error if return row count is 0', async () => {
|
||||
const { id } = mockRole;
|
||||
const { id } = mockAdminUserRole;
|
||||
|
||||
mockQuery.mockImplementationOnce(async () => {
|
||||
return createMockQueryResult([]);
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockRole, mockUser, mockRole2, mockUserRole } from '#src/__mocks__/index.js';
|
||||
import {
|
||||
mockAdminUserRole,
|
||||
mockUser,
|
||||
mockAdminUserRole2,
|
||||
mockUserRole,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import { mockId, mockStandardId } from '#src/test-utils/nanoid.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
@ -15,7 +20,7 @@ const roles = {
|
|||
findRolesByRoleIds: jest.fn(),
|
||||
findRoleById: jest.fn(),
|
||||
countRoles: jest.fn(async () => ({ count: 1 })),
|
||||
findRoles: jest.fn(async () => [mockRole]),
|
||||
findRoles: jest.fn(async () => [mockAdminUserRole]),
|
||||
};
|
||||
|
||||
const usersRoles = {
|
||||
|
@ -36,35 +41,43 @@ describe('user role routes', () => {
|
|||
findUsersRolesByUserId.mockResolvedValueOnce([]);
|
||||
const response = await roleRequester.get(`/users/${mockUser.id}/roles`);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([mockRole]);
|
||||
expect(response.body).toEqual([mockAdminUserRole]);
|
||||
});
|
||||
|
||||
it('POST /users/:id/roles', async () => {
|
||||
findUsersRolesByUserId.mockResolvedValueOnce([]);
|
||||
const response = await roleRequester.post(`/users/${mockUser.id}/roles`).send({
|
||||
roleIds: [mockRole.id],
|
||||
roleIds: [mockAdminUserRole.id],
|
||||
});
|
||||
expect(response.status).toEqual(201);
|
||||
expect(insertUsersRoles).toHaveBeenCalledWith([
|
||||
{ id: mockId, userId: mockUser.id, roleId: mockRole.id },
|
||||
{ id: mockId, userId: mockUser.id, roleId: mockAdminUserRole.id },
|
||||
]);
|
||||
});
|
||||
|
||||
it('PUT /users/:id/roles', async () => {
|
||||
findUsersRolesByUserId.mockResolvedValueOnce([mockUserRole]);
|
||||
const response = await roleRequester.put(`/users/${mockUser.id}/roles`).send({
|
||||
roleIds: [mockRole2.id],
|
||||
roleIds: [mockAdminUserRole2.id],
|
||||
});
|
||||
expect(response.status).toEqual(200);
|
||||
expect(deleteUsersRolesByUserIdAndRoleId).toHaveBeenCalledWith(mockUser.id, mockRole.id);
|
||||
expect(deleteUsersRolesByUserIdAndRoleId).toHaveBeenCalledWith(
|
||||
mockUser.id,
|
||||
mockAdminUserRole.id
|
||||
);
|
||||
expect(insertUsersRoles).toHaveBeenCalledWith([
|
||||
{ id: mockId, userId: mockUser.id, roleId: mockRole2.id },
|
||||
{ id: mockId, userId: mockUser.id, roleId: mockAdminUserRole2.id },
|
||||
]);
|
||||
});
|
||||
|
||||
it('DELETE /users/:id/roles/:roleId', async () => {
|
||||
const response = await roleRequester.delete(`/users/${mockUser.id}/roles/${mockRole.id}`);
|
||||
const response = await roleRequester.delete(
|
||||
`/users/${mockUser.id}/roles/${mockAdminUserRole.id}`
|
||||
);
|
||||
expect(response.status).toEqual(204);
|
||||
expect(deleteUsersRolesByUserIdAndRoleId).toHaveBeenCalledWith(mockUser.id, mockRole.id);
|
||||
expect(deleteUsersRolesByUserIdAndRoleId).toHaveBeenCalledWith(
|
||||
mockUser.id,
|
||||
mockAdminUserRole.id
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -109,7 +109,7 @@ export default function adminUserRoleRoutes<T extends AuthedRouter>(
|
|||
koaGuard({
|
||||
params: object({ userId: string() }),
|
||||
body: object({ roleIds: string().min(1).array() }),
|
||||
status: [200, 404],
|
||||
status: [200, 404, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { CreateUser, Role, User } from '@logto/schemas';
|
||||
import { userInfoSelectFields } from '@logto/schemas';
|
||||
import { userInfoSelectFields, RoleType } from '@logto/schemas';
|
||||
import { pickDefault } from '@logto/shared/esm';
|
||||
import { pick } from '@silverhand/essentials';
|
||||
|
||||
|
@ -35,7 +35,13 @@ const mockedQueries = {
|
|||
roles: {
|
||||
findRolesByRoleNames: jest.fn(
|
||||
async (): Promise<Role[]> => [
|
||||
{ tenantId: 'fake_tenant', id: 'role_id', name: 'admin', description: 'none' },
|
||||
{
|
||||
tenantId: 'fake_tenant',
|
||||
id: 'role_id',
|
||||
name: 'admin',
|
||||
description: 'none',
|
||||
type: RoleType.User,
|
||||
},
|
||||
]
|
||||
),
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { CreateUser, Role, SignInExperience, User } from '@logto/schemas';
|
||||
import { RoleType } from '@logto/schemas';
|
||||
import { createMockUtils, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockUser, mockUserResponse } from '#src/__mocks__/index.js';
|
||||
|
@ -44,7 +45,13 @@ const mockedQueries = {
|
|||
roles: {
|
||||
findRolesByRoleNames: jest.fn(
|
||||
async (): Promise<Role[]> => [
|
||||
{ tenantId: 'fake_tenant', id: 'role_id', name: 'admin', description: 'none' },
|
||||
{
|
||||
tenantId: 'fake_tenant',
|
||||
id: 'role_id',
|
||||
name: 'admin',
|
||||
description: 'none',
|
||||
type: RoleType.User,
|
||||
},
|
||||
]
|
||||
),
|
||||
},
|
||||
|
|
|
@ -143,7 +143,7 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
})
|
||||
),
|
||||
response: Applications.guard,
|
||||
status: [200, 404, 500],
|
||||
status: [200, 404, 422, 500],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ConnectorType } from '@logto/connector-kit';
|
||||
import { pickDefault, createMockUtils } from '@logto/shared/esm';
|
||||
|
||||
import { mockRole } from '#src/__mocks__/index.js';
|
||||
import { mockAdminUserRole } from '#src/__mocks__/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import type Libraries from '#src/tenants/Libraries.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
|
@ -49,7 +49,7 @@ const baseProviderMock = {
|
|||
};
|
||||
|
||||
const usersLibraries = {
|
||||
findUserRoles: jest.fn(async () => [mockRole]),
|
||||
findUserRoles: jest.fn(async () => [mockAdminUserRole]),
|
||||
} satisfies Partial<Libraries['users']>;
|
||||
|
||||
const tenantContext = new MockTenant(
|
||||
|
@ -66,7 +66,7 @@ const request = createRequester({
|
|||
|
||||
describe('authn route for Hasura', () => {
|
||||
const mockUserId = 'foo';
|
||||
const mockExpectedRole = mockRole.name;
|
||||
const mockExpectedRole = mockAdminUserRole.name;
|
||||
const mockUnauthorizedRole = 'V';
|
||||
const keys = Object.freeze({
|
||||
expectedRole: 'Expected-Role',
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import type { Role } from '@logto/schemas';
|
||||
import { pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockRole, mockScope, mockResource, mockScopeWithResource } from '#src/__mocks__/index.js';
|
||||
import {
|
||||
mockAdminUserRole,
|
||||
mockScope,
|
||||
mockResource,
|
||||
mockScopeWithResource,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import { mockId, mockStandardId } from '#src/test-utils/nanoid.js';
|
||||
import { createMockQuotaLibrary } from '#src/test-utils/quota.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
|
@ -12,15 +17,15 @@ const { jest } = import.meta;
|
|||
await mockStandardId();
|
||||
|
||||
const roles = {
|
||||
findRoles: jest.fn(async (): Promise<Role[]> => [mockRole]),
|
||||
findRoles: jest.fn(async (): Promise<Role[]> => [mockAdminUserRole]),
|
||||
countRoles: jest.fn(async () => ({ count: 10 })),
|
||||
insertRole: jest.fn(async (data) => ({
|
||||
...data,
|
||||
id: mockRole.id,
|
||||
id: mockAdminUserRole.id,
|
||||
})),
|
||||
findRoleById: jest.fn(),
|
||||
updateRoleById: jest.fn(async (id, data) => ({
|
||||
...mockRole,
|
||||
...mockAdminUserRole,
|
||||
...data,
|
||||
})),
|
||||
findRolesByRoleIds: jest.fn(),
|
||||
|
@ -71,40 +76,42 @@ describe('role scope routes', () => {
|
|||
const roleRequester = createRequester({ authedRoutes: roleRoutes, tenantContext });
|
||||
|
||||
it('GET /roles/:id/scopes', async () => {
|
||||
findRoleById.mockResolvedValueOnce(mockRole);
|
||||
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
|
||||
findRolesScopesByRoleId.mockResolvedValueOnce([]);
|
||||
findScopesByIds.mockResolvedValueOnce([mockScope]);
|
||||
const response = await roleRequester.get(`/roles/${mockRole.id}/scopes`);
|
||||
const response = await roleRequester.get(`/roles/${mockAdminUserRole.id}/scopes`);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([mockScopeWithResource]);
|
||||
});
|
||||
|
||||
it('GET /roles/:id/scopes (with pagination)', async () => {
|
||||
findRoleById.mockResolvedValueOnce(mockRole);
|
||||
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
|
||||
findRolesScopesByRoleId.mockResolvedValueOnce([]);
|
||||
findScopesByIds.mockResolvedValueOnce([mockScope]);
|
||||
const response = await roleRequester.get(`/roles/${mockRole.id}/scopes?page=1`);
|
||||
const response = await roleRequester.get(`/roles/${mockAdminUserRole.id}/scopes?page=1`);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([mockScopeWithResource]);
|
||||
});
|
||||
|
||||
it('POST /roles/:id/scopes', async () => {
|
||||
findRoleById.mockResolvedValueOnce(mockRole);
|
||||
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
|
||||
findRolesScopesByRoleId.mockResolvedValue([]);
|
||||
findScopesByIds.mockResolvedValueOnce([]);
|
||||
const response = await roleRequester.post(`/roles/${mockRole.id}/scopes`).send({
|
||||
const response = await roleRequester.post(`/roles/${mockAdminUserRole.id}/scopes`).send({
|
||||
scopeIds: [mockScope.id],
|
||||
});
|
||||
expect(response.status).toEqual(200);
|
||||
expect(insertRolesScopes).toHaveBeenCalledWith([
|
||||
{ id: mockId, roleId: mockRole.id, scopeId: mockScope.id },
|
||||
{ id: mockId, roleId: mockAdminUserRole.id, scopeId: mockScope.id },
|
||||
]);
|
||||
});
|
||||
|
||||
it('DELETE /roles/:id/scopes/:scopeId', async () => {
|
||||
findRoleById.mockResolvedValueOnce(mockRole);
|
||||
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
|
||||
findRolesScopesByRoleId.mockResolvedValueOnce([]);
|
||||
const response = await roleRequester.delete(`/roles/${mockRole.id}/scopes/${mockScope.id}`);
|
||||
const response = await roleRequester.delete(
|
||||
`/roles/${mockAdminUserRole.id}/scopes/${mockScope.id}`
|
||||
);
|
||||
expect(response.status).toEqual(204);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { Role } from '@logto/schemas';
|
||||
import { pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockRole, mockScope, mockUser, mockResource } from '#src/__mocks__/index.js';
|
||||
import { mockAdminUserRole, mockScope, mockUser, mockResource } from '#src/__mocks__/index.js';
|
||||
import { mockId, mockStandardId } from '#src/test-utils/nanoid.js';
|
||||
import { createMockQuotaLibrary } from '#src/test-utils/quota.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
|
@ -12,19 +12,20 @@ const { jest } = import.meta;
|
|||
await mockStandardId();
|
||||
|
||||
const roles = {
|
||||
findRoles: jest.fn(async (): Promise<Role[]> => [mockRole]),
|
||||
findRoles: jest.fn(async (): Promise<Role[]> => [mockAdminUserRole]),
|
||||
countRoles: jest.fn(async () => ({ count: 10 })),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
findRoleByRoleName: jest.fn(async (): Promise<Role | null> => null),
|
||||
insertRole: jest.fn(async (data) => ({
|
||||
type: mockAdminUserRole.type,
|
||||
...data,
|
||||
id: mockRole.id,
|
||||
id: mockAdminUserRole.id,
|
||||
tenantId: 'fake_tenant',
|
||||
})),
|
||||
deleteRoleById: jest.fn(),
|
||||
findRoleById: jest.fn(),
|
||||
updateRoleById: jest.fn(async (id, data) => ({
|
||||
...mockRole,
|
||||
...mockAdminUserRole,
|
||||
...data,
|
||||
tenantId: 'fake_tenant',
|
||||
})),
|
||||
|
@ -97,7 +98,7 @@ describe('role routes', () => {
|
|||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([
|
||||
{
|
||||
...mockRole,
|
||||
...mockAdminUserRole,
|
||||
usersCount: 1,
|
||||
featuredUsers: [
|
||||
{
|
||||
|
@ -111,87 +112,92 @@ describe('role routes', () => {
|
|||
});
|
||||
|
||||
it('POST /roles', async () => {
|
||||
const { name, description } = mockRole;
|
||||
const { name, description } = mockAdminUserRole;
|
||||
|
||||
const response = await roleRequester.post('/roles').send({ name, description });
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(mockRole);
|
||||
expect(response.body).toEqual(mockAdminUserRole);
|
||||
expect(findRoleByRoleName).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('POST /roles with scopeIds', async () => {
|
||||
const { name, description } = mockRole;
|
||||
const { name, description } = mockAdminUserRole;
|
||||
|
||||
const response = await roleRequester
|
||||
.post('/roles')
|
||||
.send({ name, description, scopeIds: [mockScope.id] });
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(mockRole);
|
||||
expect(response.body).toEqual(mockAdminUserRole);
|
||||
expect(findRoleByRoleName).toHaveBeenCalled();
|
||||
expect(findScopeById).toHaveBeenCalledWith(mockScope.id);
|
||||
expect(insertRolesScopes).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('GET /roles/:id', async () => {
|
||||
findRoleById.mockResolvedValueOnce(mockRole);
|
||||
const response = await roleRequester.get(`/roles/${mockRole.id}`);
|
||||
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
|
||||
const response = await roleRequester.get(`/roles/${mockAdminUserRole.id}`);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(mockRole);
|
||||
expect(response.body).toEqual(mockAdminUserRole);
|
||||
});
|
||||
|
||||
describe('PATCH /roles/:id', () => {
|
||||
it('updated successfully', async () => {
|
||||
findRoleById.mockResolvedValueOnce(mockRole);
|
||||
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
|
||||
const response = await roleRequester
|
||||
.patch(`/roles/${mockRole.id}`)
|
||||
.patch(`/roles/${mockAdminUserRole.id}`)
|
||||
.send({ description: 'new' });
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
...mockRole,
|
||||
...mockAdminUserRole,
|
||||
description: 'new',
|
||||
});
|
||||
});
|
||||
|
||||
it('name conflict', async () => {
|
||||
findRoleById.mockResolvedValueOnce(mockRole);
|
||||
findRoleByRoleName.mockResolvedValueOnce(mockRole);
|
||||
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
|
||||
findRoleByRoleName.mockResolvedValueOnce(mockAdminUserRole);
|
||||
const response = await roleRequester
|
||||
.patch(`/roles/${mockRole.id}`)
|
||||
.send({ name: mockRole.name });
|
||||
.patch(`/roles/${mockAdminUserRole.id}`)
|
||||
.send({ name: mockAdminUserRole.name });
|
||||
expect(response.status).toEqual(422);
|
||||
});
|
||||
});
|
||||
|
||||
it('DELETE /roles/:id', async () => {
|
||||
const response = await roleRequester.delete(`/roles/${mockRole.id}`);
|
||||
const response = await roleRequester.delete(`/roles/${mockAdminUserRole.id}`);
|
||||
expect(response.status).toEqual(204);
|
||||
expect(deleteRoleById).toHaveBeenCalledWith(mockRole.id);
|
||||
expect(deleteRoleById).toHaveBeenCalledWith(mockAdminUserRole.id);
|
||||
});
|
||||
|
||||
it('GET /roles/:id/users', async () => {
|
||||
findRoleById.mockResolvedValueOnce(mockRole);
|
||||
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
|
||||
findUsersRolesByRoleId.mockResolvedValueOnce([]);
|
||||
findUsersByIds.mockResolvedValueOnce([mockUser]);
|
||||
const response = await roleRequester.get(`/roles/${mockRole.id}/users`);
|
||||
const response = await roleRequester.get(`/roles/${mockAdminUserRole.id}/users`);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body[0]).toHaveProperty('id', mockUser.id);
|
||||
});
|
||||
|
||||
it('POST /roles/:id/users', async () => {
|
||||
findRoleById.mockResolvedValueOnce(mockRole);
|
||||
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
|
||||
findFirstUsersRolesByRoleIdAndUserIds.mockResolvedValueOnce(null);
|
||||
const response = await roleRequester.post(`/roles/${mockRole.id}/users`).send({
|
||||
const response = await roleRequester.post(`/roles/${mockAdminUserRole.id}/users`).send({
|
||||
userIds: [mockUser.id],
|
||||
});
|
||||
expect(response.status).toEqual(201);
|
||||
expect(insertUsersRoles).toHaveBeenCalledWith([
|
||||
{ id: mockId, userId: mockUser.id, roleId: mockRole.id },
|
||||
{ id: mockId, userId: mockUser.id, roleId: mockAdminUserRole.id },
|
||||
]);
|
||||
});
|
||||
|
||||
it('DELETE /roles/:id/users/:userId', async () => {
|
||||
const response = await roleRequester.delete(`/roles/${mockRole.id}/users/${mockUser.id}`);
|
||||
const response = await roleRequester.delete(
|
||||
`/roles/${mockAdminUserRole.id}/users/${mockUser.id}`
|
||||
);
|
||||
expect(response.status).toEqual(204);
|
||||
expect(deleteUsersRolesByUserIdAndRoleId).toHaveBeenCalledWith(mockUser.id, mockRole.id);
|
||||
expect(deleteUsersRolesByUserIdAndRoleId).toHaveBeenCalledWith(
|
||||
mockUser.id,
|
||||
mockAdminUserRole.id
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,15 +1,27 @@
|
|||
import type { CreateRole, Role, Scope, User } from '@logto/schemas';
|
||||
import { RoleType } from '@logto/schemas';
|
||||
|
||||
import { generateRoleName } from '#src/utils.js';
|
||||
|
||||
import { authedAdminApi } from './api.js';
|
||||
|
||||
export const createRole = async (name?: string, description?: string, scopeIds?: string[]) =>
|
||||
export const createRole = async ({
|
||||
name,
|
||||
description,
|
||||
type,
|
||||
scopeIds,
|
||||
}: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
type?: RoleType;
|
||||
scopeIds?: string[];
|
||||
}) =>
|
||||
authedAdminApi
|
||||
.post('roles', {
|
||||
json: {
|
||||
name: name ?? generateRoleName(),
|
||||
description: description ?? generateRoleName(),
|
||||
type: type ?? RoleType.User,
|
||||
scopeIds,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
AdminTenantRole,
|
||||
type Role,
|
||||
type User,
|
||||
RoleType,
|
||||
} from '@logto/schemas';
|
||||
import { conditionalArray } from '@silverhand/essentials';
|
||||
|
||||
|
@ -40,7 +41,7 @@ const createUserWithRoles = async (roleNames: string[]) => {
|
|||
const roles = await api.get('roles').json<Role[]>();
|
||||
await Promise.all(
|
||||
roles
|
||||
.filter(({ name }) => roleNames.includes(name))
|
||||
.filter(({ name, type }) => roleNames.includes(name) && type !== RoleType.MachineToMachine)
|
||||
.map(async ({ id }) =>
|
||||
api.post(`roles/${id}/users`, {
|
||||
json: { userIds: [user.id] },
|
||||
|
|
|
@ -14,7 +14,7 @@ describe('admin console user management (roles)', () => {
|
|||
|
||||
it('should assign role to user and get list successfully', async () => {
|
||||
const user = await createUserByAdmin();
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
|
||||
await assignRolesToUser(user.id, [role.id]);
|
||||
const roles = await getUserRoles(user.id);
|
||||
|
@ -23,7 +23,7 @@ describe('admin console user management (roles)', () => {
|
|||
|
||||
it('should fail when assign duplicated role to user', async () => {
|
||||
const user = await createUserByAdmin();
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
|
||||
await assignRolesToUser(user.id, [role.id]);
|
||||
await expectRejects(assignRolesToUser(user.id, [role.id]), {
|
||||
|
@ -34,7 +34,7 @@ describe('admin console user management (roles)', () => {
|
|||
|
||||
it('should delete role from user successfully', async () => {
|
||||
const user = await createUserByAdmin();
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
|
||||
await assignRolesToUser(user.id, [role.id]);
|
||||
await deleteRoleFromUser(user.id, role.id);
|
||||
|
@ -45,7 +45,7 @@ describe('admin console user management (roles)', () => {
|
|||
|
||||
it('should delete non-exist-role from user failed', async () => {
|
||||
const user = await createUserByAdmin();
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
|
||||
const response = await deleteRoleFromUser(user.id, role.id).catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode === 404).toBe(true);
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import path from 'node:path';
|
||||
|
||||
import { fetchTokenByRefreshToken } from '@logto/js';
|
||||
import { defaultManagementApi, InteractionEvent } from '@logto/schemas';
|
||||
import { defaultManagementApi, InteractionEvent, RoleType } from '@logto/schemas';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { putInteraction } from '#src/api/index.js';
|
||||
import { assignUsersToRole } from '#src/api/role.js';
|
||||
import { assignUsersToRole, createRole } from '#src/api/role.js';
|
||||
import MockClient, { defaultConfig } from '#src/client/index.js';
|
||||
import { logtoUrl } from '#src/constants.js';
|
||||
import { processSession } from '#src/helpers/client.js';
|
||||
|
@ -22,7 +22,13 @@ describe('get access token', () => {
|
|||
beforeAll(async () => {
|
||||
await createUserByAdmin(guestUsername, password);
|
||||
const user = await createUserByAdmin(username, password);
|
||||
await assignUsersToRole([user.id], defaultManagementApi.role.id);
|
||||
const { scopes } = defaultManagementApi;
|
||||
const defaultManagementApiUserRole = await createRole({
|
||||
name: 'management-api-user-role',
|
||||
type: RoleType.User,
|
||||
scopeIds: scopes.map(({ id }) => id),
|
||||
});
|
||||
await assignUsersToRole([user.id], defaultManagementApiUserRole.id);
|
||||
await enableAllPasswordSignInMethods();
|
||||
});
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { createScope } from '#src/api/scope.js';
|
|||
|
||||
describe('roles scopes', () => {
|
||||
it('should get role scopes successfully', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const resource = await createResource();
|
||||
const scope = await createScope(resource.id);
|
||||
await assignScopesToRole([scope.id], role.id);
|
||||
|
@ -28,13 +28,13 @@ describe('roles scopes', () => {
|
|||
});
|
||||
|
||||
it('should return empty if role has no scopes', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const scopes = await getRoleScopes(role.id);
|
||||
expect(scopes.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should assign scopes to role successfully', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const resource = await createResource();
|
||||
const scope1 = await createScope(resource.id);
|
||||
const scope2 = await createScope(resource.id);
|
||||
|
@ -44,13 +44,13 @@ describe('roles scopes', () => {
|
|||
});
|
||||
|
||||
it('should fail when try to assign empty scopes', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const response = await assignScopesToRole([], role.id).catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with invalid scope input', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const response = await assignScopesToRole([''], role.id).catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(400);
|
||||
});
|
||||
|
@ -65,7 +65,7 @@ describe('roles scopes', () => {
|
|||
});
|
||||
|
||||
it('should fail if scope not found', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const response = await assignScopesToRole(['not-found'], role.id).catch(
|
||||
(error: unknown) => error
|
||||
);
|
||||
|
@ -73,7 +73,7 @@ describe('roles scopes', () => {
|
|||
});
|
||||
|
||||
it('should fail if scope already assigned to role', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const resource = await createResource();
|
||||
const scope1 = await createScope(resource.id);
|
||||
const scope2 = await createScope(resource.id);
|
||||
|
@ -85,7 +85,7 @@ describe('roles scopes', () => {
|
|||
});
|
||||
|
||||
it('should remove scope from role successfully', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const resource = await createResource();
|
||||
const scope = await createScope(resource.id);
|
||||
await assignScopesToRole([scope.id], role.id);
|
||||
|
@ -99,7 +99,7 @@ describe('roles scopes', () => {
|
|||
});
|
||||
|
||||
it('should fail when try to remove scope from role that is not assigned', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const resource = await createResource();
|
||||
const scope = await createScope(resource.id);
|
||||
const response = await deleteScopeFromRole(scope.id, role.id).catch((error: unknown) => error);
|
||||
|
|
|
@ -14,7 +14,7 @@ import { generateRoleName } from '#src/utils.js';
|
|||
|
||||
describe('roles', () => {
|
||||
it('should get roles list successfully', async () => {
|
||||
await createRole();
|
||||
await createRole({});
|
||||
const roles = await getRoles();
|
||||
|
||||
expect(roles.length > 0).toBeTruthy();
|
||||
|
@ -24,7 +24,7 @@ describe('roles', () => {
|
|||
const roleName = generateRoleName();
|
||||
const description = roleName;
|
||||
|
||||
const role = await createRole(roleName, description);
|
||||
const role = await createRole({ name: roleName, description });
|
||||
|
||||
expect(role.name).toBe(roleName);
|
||||
expect(role.description).toBe(description);
|
||||
|
@ -36,7 +36,7 @@ describe('roles', () => {
|
|||
const resource = await createResource();
|
||||
const scope = await createScope(resource.id);
|
||||
|
||||
const role = await createRole(roleName, description, [scope.id]);
|
||||
const role = await createRole({ name: roleName, description, scopeIds: [scope.id] });
|
||||
const scopes = await getRoleScopes(role.id);
|
||||
|
||||
expect(role.name).toBe(roleName);
|
||||
|
@ -45,20 +45,20 @@ describe('roles', () => {
|
|||
});
|
||||
|
||||
it('should fail when create role with conflict name', async () => {
|
||||
const createdRole = await createRole();
|
||||
const { name } = await createRole({});
|
||||
|
||||
const response = await createRole(createdRole.name).catch((error: unknown) => error);
|
||||
const response = await createRole({ name }).catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(422);
|
||||
});
|
||||
|
||||
it('should fail when try to create an internal role', async () => {
|
||||
const response = await createRole('#internal:foo').catch((error: unknown) => error);
|
||||
const response = await createRole({ name: '#internal:foo' }).catch((error: unknown) => error);
|
||||
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('should get role detail successfully', async () => {
|
||||
const createdRole = await createRole();
|
||||
const createdRole = await createRole({});
|
||||
const role = await getRole(createdRole.id);
|
||||
|
||||
expect(role.name).toBe(createdRole.name);
|
||||
|
@ -72,7 +72,7 @@ describe('roles', () => {
|
|||
});
|
||||
|
||||
it('should update role details successfully', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
|
||||
const newName = `new_${role.name}`;
|
||||
const newDescription = `new_${role.description}`;
|
||||
|
@ -89,8 +89,8 @@ describe('roles', () => {
|
|||
});
|
||||
|
||||
it('should fail when update role with conflict name', async () => {
|
||||
const role1 = await createRole();
|
||||
const role2 = await createRole();
|
||||
const role1 = await createRole({});
|
||||
const role2 = await createRole({});
|
||||
const response = await updateRole(role2.id, {
|
||||
name: role1.name,
|
||||
}).catch((error: unknown) => error);
|
||||
|
@ -105,7 +105,7 @@ describe('roles', () => {
|
|||
});
|
||||
|
||||
it('should fail when try to update an internal role', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
|
||||
const response = await updateRole(role.id, {
|
||||
name: '#internal:foo',
|
||||
|
@ -115,7 +115,7 @@ describe('roles', () => {
|
|||
});
|
||||
|
||||
it('should delete role successfully', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
|
||||
await deleteRole(role.id);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { generateNewUserProfile } from '#src/helpers/user.js';
|
|||
|
||||
describe('roles users', () => {
|
||||
it('should get role users successfully', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const user = await createUser(generateNewUserProfile({}));
|
||||
await assignUsersToRole([user.id], role.id);
|
||||
const users = await getRoleUsers(role.id);
|
||||
|
@ -21,7 +21,7 @@ describe('roles users', () => {
|
|||
});
|
||||
|
||||
it('should assign users to role successfully', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const user1 = await createUser(generateNewUserProfile({}));
|
||||
const user2 = await createUser(generateNewUserProfile({}));
|
||||
await assignUsersToRole([user1.id, user2.id], role.id);
|
||||
|
@ -31,13 +31,13 @@ describe('roles users', () => {
|
|||
});
|
||||
|
||||
it('should fail when try to assign empty users', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const response = await assignUsersToRole([], role.id).catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with invalid user input', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const response = await assignUsersToRole([''], role.id).catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(400);
|
||||
});
|
||||
|
@ -51,7 +51,7 @@ describe('roles users', () => {
|
|||
});
|
||||
|
||||
it('should fail if user not found', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const response = await assignUsersToRole(['not-found'], role.id).catch(
|
||||
(error: unknown) => error
|
||||
);
|
||||
|
@ -59,7 +59,7 @@ describe('roles users', () => {
|
|||
});
|
||||
|
||||
it('should remove user from role successfully', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const user = await createUser(generateNewUserProfile({}));
|
||||
await assignUsersToRole([user.id], role.id);
|
||||
const users = await getRoleUsers(role.id);
|
||||
|
@ -80,7 +80,7 @@ describe('roles users', () => {
|
|||
});
|
||||
|
||||
it('should fail if user not found when trying to remove user from role', async () => {
|
||||
const role = await createRole();
|
||||
const role = await createRole({});
|
||||
const response = await deleteUserFromRole('not-found', role.id).catch(
|
||||
(error: unknown) => error
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: 'Ungültige Eingabe. Wertliste darf nicht leer sein.',
|
||||
create_failed: 'Fehler beim Erstellen von {{name}}.',
|
||||
db_constraint_violated: 'Datenbankbeschränkung verletzt.',
|
||||
not_exists: '{{name}} existiert nicht.',
|
||||
not_exists_with_id: '{{name}} mit ID `{{id}}` existiert nicht.',
|
||||
not_found: 'Die Ressource wurde nicht gefunden.',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: 'Invalid input. Value list must not be empty.',
|
||||
create_failed: 'Failed to create {{name}}.',
|
||||
db_constraint_violated: 'Database constraint violated.',
|
||||
not_exists: 'The {{name}} does not exist.',
|
||||
not_exists_with_id: 'The {{name}} with ID `{{id}}` does not exist.',
|
||||
not_found: 'The resource does not exist.',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: 'Entrada no válida. La lista de valores no debe estar vacía.',
|
||||
create_failed: 'Fallo al crear {{name}}.',
|
||||
db_constraint_violated: 'Viólación de restricción de base de datos.',
|
||||
not_exists: 'El {{name}} no existe.',
|
||||
not_exists_with_id: 'El {{name}} con ID `{{id}}` no existe.',
|
||||
not_found: 'El recurso no existe.',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: 'Saisie invalide. La liste des valeurs ne doit pas être vide.',
|
||||
create_failed: 'Échec de la création de {{name}}.',
|
||||
db_constraint_violated: 'Violations de contraintes de base de données.',
|
||||
not_exists: "Le {{name}} n'existe pas.",
|
||||
not_exists_with_id: "Le {{name}} avec l'ID `{{id}}` n'existe pas.",
|
||||
not_found: "La ressource n'existe pas.",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: 'Input non valido. La lista dei valori non deve essere vuota.',
|
||||
create_failed: 'Impossibile creare {{name}}.',
|
||||
db_constraint_violated: 'Vincolo del database violato.',
|
||||
not_exists: '{{name}} non esiste.',
|
||||
not_exists_with_id: '{{name}} con ID `{{id}}` non esiste.',
|
||||
not_found: 'La risorsa non esiste.',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: '入力が無効です。値のリストは空であってはなりません。',
|
||||
create_failed: '{{name}}の作成に失敗しました。',
|
||||
db_constraint_violated: 'データベースの制約が違反しました。',
|
||||
not_exists: '{{name}}は存在しません。',
|
||||
not_exists_with_id: 'IDが`{{id}}`の{{name}}は存在しません。',
|
||||
not_found: 'リソースが存在しません。',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: '입력이 잘못되었습니다. 값 목록은 비어 있을 수 없습니다.',
|
||||
create_failed: '{{name}} 생성을 실패하였어요.',
|
||||
db_constraint_violated: '데이터베이스 제약 조건 위반.',
|
||||
not_exists: '{{name}}는 존재하지 않아요.',
|
||||
not_exists_with_id: '{{id}} ID를 가진 {{name}}는 존재하지 않아요.',
|
||||
not_found: '리소스가 존재하지 않아요.',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: 'Nieprawidłowe dane. Lista wartości nie może być pusta.',
|
||||
create_failed: 'Nie udało się utworzyć {{name}}.',
|
||||
db_constraint_violated: 'Constraint naruszenie bazy danych.',
|
||||
not_exists: '{{name}} nie istnieje.',
|
||||
not_exists_with_id: '{{name}} o identyfikatorze `{{id}}` nie istnieje.',
|
||||
not_found: 'Zasób nie istnieje.',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: 'Entrada inválida. A lista de valores não deve estar vazia.',
|
||||
create_failed: 'Falha ao criar {{name}}.',
|
||||
db_constraint_violated: 'Violação de restrição do banco de dados.',
|
||||
not_exists: 'O {{name}} não existe.',
|
||||
not_exists_with_id: 'O {{name}} com ID `{{id}}` não existe.',
|
||||
not_found: 'O recurso não existe.',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: 'Entrada inválida. A lista de valores não deve estar vazia.',
|
||||
create_failed: 'Falha ao criar {{name}}.',
|
||||
db_constraint_violated: 'Restrição do banco de dados violada.',
|
||||
not_exists: '{{name}} não existe.',
|
||||
not_exists_with_id: '{{name}} com o ID `{{id}}` não existe.',
|
||||
not_found: 'O recurso não existe.',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: 'Неверный ввод. Список значений не должен быть пустым.',
|
||||
create_failed: 'Не удалось создать {{name}}.',
|
||||
db_constraint_violated: 'Нарушено ограничение базы данных.',
|
||||
not_exists: '{{name}} не существует.',
|
||||
not_exists_with_id: '{{name}} с ID `{{id}}` не существует.',
|
||||
not_found: 'Ресурс не существует.',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: 'Geçersiz giriş. Değer listesi boş olmamalıdır.',
|
||||
create_failed: '{{name}} oluşturulamadı.',
|
||||
db_constraint_violated: 'Veritabanı kısıtı ihlal edildi.',
|
||||
not_exists: '{{name}} mevcut değil.',
|
||||
not_exists_with_id: ' `{{id}}` id kimliğine sahip {{name}} mevcut değil.',
|
||||
not_found: 'Kaynak mevcut değil.',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: '无效输入。值列表不能为空。',
|
||||
create_failed: '创建 {{name}} 失败。',
|
||||
db_constraint_violated: '数据库约束被破坏。',
|
||||
not_exists: '该 {{name}} 不存在。',
|
||||
not_exists_with_id: 'ID 为 `{{id}}` 的 {{name}} 不存在。',
|
||||
not_found: '该资源不存在。',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: '無效輸入。值列表不能為空。',
|
||||
create_failed: '創建 {{name}} 失敗。',
|
||||
db_constraint_violated: '數據庫約束違反。',
|
||||
not_exists: '該 {{name}} 不存在。',
|
||||
not_exists_with_id: 'ID 為 `{{id}}` 的 {{name}} 不存在。',
|
||||
not_found: '該資源不存在。',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const entity = {
|
||||
invalid_input: '無效的輸入。值列表不能為空。',
|
||||
create_failed: '建立 {{name}} 失敗。',
|
||||
db_constraint_violated: '數據庫限制違反。',
|
||||
not_exists: '{{name}} 不存在。',
|
||||
not_exists_with_id: 'ID 為 `{{id}}` 的 {{name}} 不存在。',
|
||||
not_found: '資源不存在。',
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import { sql } from 'slonik';
|
||||
|
||||
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||
|
||||
enum InternalRole {
|
||||
Admin = '#internal:admin',
|
||||
}
|
||||
|
||||
const alteration: AlterationScript = {
|
||||
up: async (pool) => {
|
||||
// Get all m2m role ids.
|
||||
const { rows: m2mRoleIds } = await pool.query<{ id: string }>(sql`
|
||||
select roles.id as "id" from roles
|
||||
left join applications_roles on applications_roles.role_id = roles.id and applications_roles.tenant_id = roles.tenant_id
|
||||
left join applications on applications.id = applications_roles.application_id and applications.tenant_id = applications_roles.tenant_id
|
||||
where applications.type = 'MachineToMachine' or roles.name = ${InternalRole.Admin} group by roles.id;
|
||||
`);
|
||||
// Add `type` column to `roles` table, and set `type` to 'MachineToMachine' for all m2m roles.
|
||||
await pool.query(sql`
|
||||
create type role_type as enum ('User', 'MachineToMachine');
|
||||
`);
|
||||
await pool.query(sql`alter table roles add column type role_type not null default 'User';`);
|
||||
await pool.query(sql`
|
||||
update roles set type = 'MachineToMachine' where id in (${sql.join(
|
||||
m2mRoleIds.map(({ id }) => id),
|
||||
sql`, `
|
||||
)});
|
||||
`);
|
||||
// Add role type check function and constraints for recording user/application-role relations.
|
||||
await pool.query(sql`
|
||||
create function check_role_type(role_id varchar(21), target_type role_type) returns boolean as
|
||||
$$ begin
|
||||
return (select type from roles where id = role_id) = target_type;
|
||||
end; $$ language plpgsql;
|
||||
`);
|
||||
await pool.query(sql`
|
||||
alter table users_roles add constraint users_roles__role_type
|
||||
check (check_role_type(role_id, 'User'));
|
||||
`);
|
||||
await pool.query(
|
||||
sql`alter table applications_roles add constraint applications_roles__role_type check (check_role_type(role_id, 'MachineToMachine'));`
|
||||
);
|
||||
},
|
||||
down: async (pool) => {
|
||||
await pool.query(
|
||||
sql`alter table applications_roles drop constraint applications_roles__role_type;`
|
||||
);
|
||||
await pool.query(sql`alter table users_roles drop constraint users_roles__role_type;`);
|
||||
await pool.query(sql`drop function check_role_type;`);
|
||||
await pool.query(sql`alter table roles drop column type;`);
|
||||
await pool.query(sql`drop type role_type;`);
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
|
@ -1,6 +1,7 @@
|
|||
import { generateStandardId } from '@logto/shared/universal';
|
||||
|
||||
import type { CreateScope, Role } from '../index.js';
|
||||
import { RoleType } from '../db-entries/index.js';
|
||||
import type { CreateScope, Role } from '../db-entries/index.js';
|
||||
import { AdminTenantRole } from '../types/index.js';
|
||||
|
||||
import type { UpdateAdminData } from './management-api.js';
|
||||
|
@ -80,4 +81,5 @@ export const createTenantApplicationRole = (): Readonly<Role> => ({
|
|||
name: AdminTenantRole.TenantApplication,
|
||||
description:
|
||||
'The role for M2M applications that represent a user tenant and send requests to Logto Cloud.',
|
||||
type: RoleType.MachineToMachine,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { generateStandardId } from '@logto/shared/universal';
|
||||
|
||||
import type { CreateResource, CreateRole, CreateScope } from '../db-entries/index.js';
|
||||
import {
|
||||
RoleType,
|
||||
type CreateResource,
|
||||
type CreateRole,
|
||||
type CreateScope,
|
||||
} from '../db-entries/index.js';
|
||||
import { PredefinedScope, InternalRole, AdminTenantRole } from '../types/index.js';
|
||||
|
||||
import { adminTenantId, defaultTenantId } from './tenant.js';
|
||||
|
@ -52,6 +57,7 @@ export const defaultManagementApi = Object.freeze({
|
|||
id: 'admin-role',
|
||||
name: InternalRole.Admin,
|
||||
description: `Internal admin role for Logto tenant ${defaultTenantId}.`,
|
||||
type: RoleType.MachineToMachine,
|
||||
},
|
||||
}) satisfies AdminData;
|
||||
|
||||
|
@ -95,6 +101,7 @@ export const createAdminData = (tenantId: string): AdminData => {
|
|||
id: generateStandardId(),
|
||||
name: InternalRole.Admin,
|
||||
description: `Internal admin role for Logto tenant ${defaultTenantId}.`,
|
||||
type: RoleType.MachineToMachine,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -124,6 +131,7 @@ export const createAdminDataInAdminTenant = (tenantId: string): AdminData => {
|
|||
id: generateStandardId(),
|
||||
name: getManagementApiAdminName(tenantId),
|
||||
description: `Admin tenant admin role for Logto tenant ${tenantId}.`,
|
||||
type: RoleType.User,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -152,6 +160,7 @@ export const createMeApiInAdminTenant = (): AdminData => {
|
|||
id: generateStandardId(),
|
||||
name: AdminTenantRole.User,
|
||||
description: 'Default role for admin tenant.',
|
||||
type: RoleType.User,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* init_order = 2 */
|
||||
|
||||
create table applications_roles (
|
||||
tenant_id varchar(21) not null
|
||||
references tenants (id) on update cascade on delete cascade,
|
||||
|
@ -8,7 +10,9 @@ create table applications_roles (
|
|||
references roles (id) on update cascade on delete cascade,
|
||||
primary key (id),
|
||||
constraint applications_roles__application_id_role_id
|
||||
unique (tenant_id, application_id, role_id)
|
||||
unique (tenant_id, application_id, role_id),
|
||||
constraint applications_roles__role_type
|
||||
check (check_role_type(role_id, 'MachineToMachine'))
|
||||
);
|
||||
|
||||
create index applications_roles__id
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
/* init_order = 1 */
|
||||
|
||||
create type role_type as enum ('User', 'MachineToMachine');
|
||||
|
||||
create table roles (
|
||||
tenant_id varchar(21) not null
|
||||
references tenants (id) on update cascade on delete cascade,
|
||||
id varchar(21) not null,
|
||||
name varchar(128) not null,
|
||||
description varchar(128) not null,
|
||||
type role_type not null default 'User',
|
||||
primary key (id),
|
||||
constraint roles__name
|
||||
unique (tenant_id, name)
|
||||
|
@ -13,3 +16,8 @@ create table roles (
|
|||
|
||||
create index roles__id
|
||||
on roles (tenant_id, id);
|
||||
|
||||
create function check_role_type(role_id varchar(21), target_type role_type) returns boolean as
|
||||
$$ begin
|
||||
return (select type from roles where id = role_id) = target_type;
|
||||
end; $$ language plpgsql;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* init_order = 2 */
|
||||
|
||||
create table users_roles (
|
||||
tenant_id varchar(21) not null
|
||||
references tenants (id) on update cascade on delete cascade,
|
||||
|
@ -8,7 +10,9 @@ create table users_roles (
|
|||
references roles (id) on update cascade on delete cascade,
|
||||
primary key (id),
|
||||
constraint users_roles__user_id_role_id
|
||||
unique (tenant_id, user_id, role_id)
|
||||
unique (tenant_id, user_id, role_id),
|
||||
constraint users_roles__role_type
|
||||
check (check_role_type(role_id, 'User'))
|
||||
);
|
||||
|
||||
create index users_roles__id
|
||||
|
|
Loading…
Reference in a new issue