0
Fork 0
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:
Darcy Ye 2023-09-11 11:27:49 +08:00 committed by GitHub
parent d1b92e99aa
commit 5d78c7271b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 325 additions and 136 deletions

View 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.

View file

@ -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',

View file

@ -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 = {

View file

@ -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]);
});
});

View file

@ -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',

View file

@ -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([]);

View file

@ -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
);
});
});

View file

@ -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 {

View file

@ -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,
},
]
),
},

View file

@ -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,
},
]
),
},

View file

@ -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 {

View file

@ -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',

View file

@ -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);
});
});

View file

@ -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
);
});
});

View file

@ -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,
},
})

View file

@ -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] },

View file

@ -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);

View file

@ -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();
});

View file

@ -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);

View file

@ -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);

View file

@ -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
);

View file

@ -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.',

View file

@ -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.',

View file

@ -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.',

View file

@ -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.",

View file

@ -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.',

View file

@ -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: 'リソースが存在しません。',

View file

@ -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: '리소스가 존재하지 않아요.',

View file

@ -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.',

View file

@ -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.',

View file

@ -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.',

View file

@ -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: 'Ресурс не существует.',

View file

@ -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.',

View file

@ -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: '该资源不存在。',

View file

@ -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: '該資源不存在。',

View file

@ -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: '資源不存在。',

View file

@ -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;

View file

@ -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,
});

View file

@ -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,
},
});
};

View file

@ -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

View file

@ -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;

View file

@ -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