0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

fix(core): mgmt API scopes are not assignable to user roles (#5425)

* fix(core): mgmt API scopes are not assignable to user roles

* feat: add alteration script to remove mgmt api scopes already assigned to user role

* chore: use option object
This commit is contained in:
Darcy Ye 2024-02-27 11:19:59 +08:00 committed by GitHub
parent 2a99ba5997
commit 873d20fed9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 237 additions and 56 deletions

View file

@ -0,0 +1,64 @@
import { isManagementApi, RoleType } from '@logto/schemas';
import RequestError from '#src/errors/RequestError/index.js';
import type Queries from '#src/tenants/Queries.js';
import assertThat from '#src/utils/assert-that.js';
export const createRoleScopeLibrary = (queries: Queries) => {
const {
roles: { findRoleById },
scopes: { findScopeById },
resources: { findResourceById },
rolesScopes: { findRolesScopesByRoleId },
} = queries;
const validateRoleScopeAssignment = async (
scopeIds: string[],
roleId: string,
options: { skipScopeExistenceCheck?: boolean } = {}
) => {
// No need to validate if no scopes are being assigned.
if (scopeIds.length === 0) {
return;
}
const { skipScopeExistenceCheck } = options;
const role = await findRoleById(roleId);
// Make sure all scopes have not been assigned to the role.
// The check can be skipped if the role is newly created.
if (!skipScopeExistenceCheck) {
const rolesScopes = await findRolesScopesByRoleId(roleId);
for (const scopeId of scopeIds) {
assertThat(
!rolesScopes.some(({ scopeId: _scopeId }) => _scopeId === scopeId),
new RequestError({
code: 'role.scope_exists',
status: 422,
scopeId,
})
);
}
}
await Promise.all(
scopeIds.map(async (scopeId) => {
// 1. Make sure the `scopeId` is valid.
const { resourceId } = await findScopeById(scopeId);
// 2. Make sure management API scopes can not be assigned to user roles.
if (role.type === RoleType.User) {
const { indicator } = await findResourceById(resourceId);
assertThat(
!isManagementApi(indicator),
'role.management_api_scopes_not_assignable_to_user_role'
);
}
})
);
};
return {
validateRoleScopeAssignment,
};
};

View file

@ -55,6 +55,11 @@ const users = {
findUserById: jest.fn(),
};
const rolesScopesLibrary = {
validateRoleScopeAssignment: jest.fn(),
};
const { validateRoleScopeAssignment } = rolesScopesLibrary;
const roleRoutes = await pickDefault(import('./role.scope.js'));
const tenantContext = new MockTenant(
@ -69,6 +74,7 @@ const tenantContext = new MockTenant(
undefined,
{
quota: createMockQuotaLibrary(),
roleScopes: rolesScopesLibrary,
}
);
@ -94,13 +100,13 @@ describe('role scope routes', () => {
});
it('POST /roles/:id/scopes', async () => {
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
findRolesScopesByRoleId.mockResolvedValue([]);
findScopesByIds.mockResolvedValueOnce([]);
const response = await roleRequester.post(`/roles/${mockAdminUserRole.id}/scopes`).send({
scopeIds: [mockScope.id],
});
expect(response.status).toEqual(200);
expect(validateRoleScopeAssignment).toHaveBeenCalledWith([mockScope.id], mockAdminUserRole.id);
expect(insertRolesScopes).toHaveBeenCalledWith([
{ id: mockId, roleId: mockAdminUserRole.id, scopeId: mockScope.id },
]);

View file

@ -13,20 +13,18 @@ import { parseSearchParamsForSearch } from '#src/utils/search.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
export default function roleScopeRoutes<T extends AuthedRouter>(
...[
router,
{
queries,
libraries: { quota },
},
]: RouterInitArgs<T>
...[router, { queries, libraries }]: RouterInitArgs<T>
) {
const {
resources: { findResourcesByIds },
rolesScopes: { deleteRolesScope, findRolesScopesByRoleId, insertRolesScopes },
roles: { findRoleById },
scopes: { findScopeById, findScopesByIds, countScopesByScopeIds, searchScopesByScopeIds },
scopes: { findScopesByIds, countScopesByScopeIds, searchScopesByScopeIds },
} = queries;
const {
quota,
roleScopes: { validateRoleScopeAssignment },
} = libraries;
const attachResourceToScopes = async (scopes: readonly Scope[]): Promise<ScopeResponse[]> => {
const resources = await findResourcesByIds(scopes.map(({ resourceId }) => resourceId));
@ -103,7 +101,7 @@ export default function roleScopeRoutes<T extends AuthedRouter>(
params: object({ id: string().min(1) }),
body: object({ scopeIds: string().min(1).array().nonempty() }),
response: Scopes.guard.array(),
status: [200, 404, 422],
status: [200, 400, 404, 422],
}),
async (ctx, next) => {
const {
@ -111,24 +109,9 @@ export default function roleScopeRoutes<T extends AuthedRouter>(
body: { scopeIds },
} = ctx.guard;
await findRoleById(id);
await quota.guardKey('scopesPerRoleLimit', id);
const rolesScopes = await findRolesScopesByRoleId(id);
for (const scopeId of scopeIds) {
assertThat(
!rolesScopes.some(({ scopeId: _scopeId }) => _scopeId === scopeId),
new RequestError({
code: 'role.scope_exists',
status: 422,
scopeId,
})
);
}
await Promise.all(scopeIds.map(async (scopeId) => findScopeById(scopeId)));
await validateRoleScopeAssignment(scopeIds, id);
await insertRolesScopes(
scopeIds.map((scopeId) => ({ id: generateStandardId(), roleId: id, scopeId }))
);

View file

@ -35,7 +35,6 @@ const { findRoleByRoleName, findRoleById, deleteRoleById } = roles;
const scopes = {
findScopeById: jest.fn(),
};
const { findScopeById } = scopes;
const rolesScopes = {
insertRolesScopes: jest.fn(),
@ -63,6 +62,11 @@ const applicationsRoles = {
};
const { countApplicationsRolesByRoleId, findApplicationsRolesByRoleId } = applicationsRoles;
const rolesScopesLibrary = {
validateRoleScopeAssignment: jest.fn(),
};
const { validateRoleScopeAssignment } = rolesScopesLibrary;
const roleRoutes = await pickDefault(import('./role.js'));
const tenantContext = new MockTenant(
@ -77,7 +81,7 @@ const tenantContext = new MockTenant(
applications,
},
undefined,
{ quota: createMockQuotaLibrary() }
{ quota: createMockQuotaLibrary(), roleScopes: rolesScopesLibrary }
);
describe('role routes', () => {
@ -127,7 +131,9 @@ describe('role routes', () => {
expect(response.status).toEqual(200);
expect(response.body).toEqual(mockAdminUserRole);
expect(findRoleByRoleName).toHaveBeenCalled();
expect(findScopeById).toHaveBeenCalledWith(mockScope.id);
expect(validateRoleScopeAssignment).toHaveBeenCalledWith([mockScope.id], response.body.id, {
skipScopeExistenceCheck: true,
});
expect(insertRolesScopes).toHaveBeenCalled();
});

View file

@ -16,10 +16,7 @@ import roleUserRoutes from './role.user.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
export default function roleRoutes<T extends AuthedRouter>(...[router, tenant]: RouterInitArgs<T>) {
const {
queries,
libraries: { quota },
} = tenant;
const { queries, libraries } = tenant;
const {
rolesScopes: { insertRolesScopes },
roles: {
@ -31,7 +28,6 @@ export default function roleRoutes<T extends AuthedRouter>(...[router, tenant]:
insertRole,
updateRoleById,
},
scopes: { findScopeById },
users: { findUsersByIds },
usersRoles: { countUsersRolesByRoleId, findUsersRolesByRoleId, findUsersRolesByUserId },
applications: { findApplicationsByIds },
@ -41,6 +37,10 @@ export default function roleRoutes<T extends AuthedRouter>(...[router, tenant]:
findApplicationsRolesByApplicationId,
},
} = queries;
const {
quota,
roleScopes: { validateRoleScopeAssignment },
} = libraries;
router.use('/roles(/.*)?', koaRoleRlsErrorHandler());
@ -136,7 +136,7 @@ export default function roleRoutes<T extends AuthedRouter>(...[router, tenant]:
body: Roles.createGuard
.omit({ id: true })
.extend({ scopeIds: z.string().min(1).array().optional() }),
status: [200, 422],
status: [200, 400, 404, 422], // Throws 404 when invalid `scopeId(s)` are provided.
response: Roles.guard,
}),
async (ctx, next) => {
@ -165,7 +165,8 @@ export default function roleRoutes<T extends AuthedRouter>(...[router, tenant]:
});
if (scopeIds) {
await Promise.all(scopeIds.map(async (scopeId) => findScopeById(scopeId)));
// Skip scope existence check because the role is newly created.
await validateRoleScopeAssignment(scopeIds, role.id, { skipScopeExistenceCheck: true });
await insertRolesScopes(
scopeIds.map((scopeId) => ({ id: generateStandardId(), roleId: role.id, scopeId }))
);

View file

@ -8,6 +8,7 @@ import { createPasscodeLibrary } from '#src/libraries/passcode.js';
import { createPhraseLibrary } from '#src/libraries/phrase.js';
import { createProtectedAppLibrary } from '#src/libraries/protected-app.js';
import { createQuotaLibrary } from '#src/libraries/quota.js';
import { createRoleScopeLibrary } from '#src/libraries/role-scope.js';
import { createSignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js';
import { createSocialLibrary } from '#src/libraries/social.js';
import { createSsoConnectorLibrary } from '#src/libraries/sso-connector.js';
@ -24,6 +25,7 @@ export default class Libraries {
passcodes = createPasscodeLibrary(this.queries, this.connectors);
applications = createApplicationLibrary(this.queries);
verificationStatuses = createVerificationStatusLibrary(this.queries);
roleScopes = createRoleScopeLibrary(this.queries);
domains = createDomainLibrary(this.queries);
protectedApps = createProtectedAppLibrary(this.queries);
quota = createQuotaLibrary(this.queries, this.cloudConnection, this.connectors);

View file

@ -1,12 +1,13 @@
import path from 'node:path';
import { fetchTokenByRefreshToken } from '@logto/js';
import { defaultManagementApi, InteractionEvent, RoleType } from '@logto/schemas';
import { InteractionEvent, type Resource, RoleType } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import fetch from 'node-fetch';
import { putInteraction } from '#src/api/index.js';
import { createResource, putInteraction } from '#src/api/index.js';
import { assignUsersToRole, createRole } from '#src/api/role.js';
import { createScope } from '#src/api/scope.js';
import MockClient, { defaultConfig } from '#src/client/index.js';
import { logtoUrl } from '#src/constants.js';
import { processSession } from '#src/helpers/client.js';
@ -18,24 +19,35 @@ describe('get access token', () => {
const username = generateUsername();
const password = generatePassword();
const guestUsername = generateUsername();
const testApiResourceInfo: Pick<Resource, 'name' | 'indicator'> = {
name: 'test-api-resource',
indicator: 'https://foo.logto.io/api',
};
const testApiScopeNames = ['read', 'write', 'delete', 'update'];
beforeAll(async () => {
await createUserByAdmin(guestUsername, password);
const user = await createUserByAdmin(username, password);
const { scopes } = defaultManagementApi;
const defaultManagementApiUserRole = await createRole({
name: 'management-api-user-role',
const testApiResource = await createResource(
testApiResourceInfo.name,
testApiResourceInfo.indicator
);
const testApiScopes = await Promise.all(
testApiScopeNames.map(async (name) => createScope(testApiResource.id, name))
);
const testApiUserRole = await createRole({
name: 'test-api-user-role',
type: RoleType.User,
scopeIds: scopes.map(({ id }) => id),
scopeIds: testApiScopes.map(({ id }) => id),
});
await assignUsersToRole([user.id], defaultManagementApiUserRole.id);
await assignUsersToRole([user.id], testApiUserRole.id);
await enableAllPasswordSignInMethods();
});
it('can sign in and getAccessToken with admin user', async () => {
const client = new MockClient({
resources: [defaultManagementApi.resource.indicator],
scopes: defaultManagementApi.scopes.map(({ name }) => name),
resources: [testApiResourceInfo.indicator],
scopes: testApiScopeNames,
});
await client.initSession();
await client.successSend(putInteraction, {
@ -44,12 +56,9 @@ describe('get access token', () => {
});
const { redirectTo } = await client.submitInteraction();
await processSession(client, redirectTo);
const accessToken = await client.getAccessToken(defaultManagementApi.resource.indicator);
const accessToken = await client.getAccessToken(testApiResourceInfo.indicator);
expect(accessToken).not.toBeNull();
expect(getAccessTokenPayload(accessToken)).toHaveProperty(
'scope',
defaultManagementApi.scopes.map(({ name }) => name).join(' ')
);
expect(getAccessTokenPayload(accessToken)).toHaveProperty('scope', testApiScopeNames.join(' '));
// Request for invalid resource should throw
void expect(client.getAccessToken('api.foo.com')).rejects.toThrow();
@ -57,8 +66,8 @@ describe('get access token', () => {
it('can sign in and getAccessToken with guest user', async () => {
const client = new MockClient({
resources: [defaultManagementApi.resource.indicator],
scopes: defaultManagementApi.scopes.map(({ name }) => name),
resources: [testApiResourceInfo.indicator],
scopes: testApiScopeNames,
});
await client.initSession();
await client.successSend(putInteraction, {
@ -67,16 +76,16 @@ describe('get access token', () => {
});
const { redirectTo } = await client.submitInteraction();
await processSession(client, redirectTo);
const accessToken = await client.getAccessToken(defaultManagementApi.resource.indicator);
const accessToken = await client.getAccessToken(testApiResourceInfo.indicator);
expect(getAccessTokenPayload(accessToken)).not.toHaveProperty(
'scope',
defaultManagementApi.scopes.map(({ name }) => name).join(' ')
testApiScopeNames.join(' ')
);
});
it('can sign in and get multiple Access Tokens by the same Refresh Token within refreshTokenReuseInterval', async () => {
const client = new MockClient({ resources: [defaultManagementApi.resource.indicator] });
const client = new MockClient({ resources: [testApiResourceInfo.indicator] });
await client.initSession();
@ -99,7 +108,7 @@ describe('get access token', () => {
clientId: defaultConfig.appId,
tokenEndpoint: path.join(logtoUrl, '/oidc/token'),
refreshToken,
resource: defaultManagementApi.resource.indicator,
resource: testApiResourceInfo.indicator,
},
async <T>(...args: Parameters<typeof fetch>): Promise<T> => {
const response = await fetch(...args);

View file

@ -84,6 +84,16 @@ describe('roles scopes', () => {
expect(response instanceof HTTPError && response.response.statusCode).toBe(422);
});
it('should fail if try to assign management API scope(s) to user role', async () => {
// Create `RoleType.User` role by default if `type` is not specified.
const userRole = await createRole({});
const response = await assignScopesToRole(
[defaultManagementApi.scopes[0]!.id],
userRole.id
).catch((error: unknown) => error);
expect(response instanceof HTTPError && response.response.statusCode).toBe(400);
});
it('should remove scope from role successfully', async () => {
const role = await createRole({});
const resource = await createResource();

View file

@ -1,3 +1,4 @@
import { defaultManagementApi } from '@logto/schemas';
import { HTTPError } from 'got';
import { createResource } from '#src/api/resource.js';
@ -57,6 +58,14 @@ describe('roles', () => {
expect(response instanceof HTTPError && response.response.statusCode).toBe(403);
});
it('should fail when try to create role with management API scope(s)', async () => {
const response = await createRole({ scopeIds: [defaultManagementApi.scopes[0]!.id] }).catch(
(error: unknown) => error
);
expect(response instanceof HTTPError && response.response.statusCode).toBe(400);
});
it('should get role detail successfully', async () => {
const createdRole = await createRole({});
const role = await getRole(createdRole.id);

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: 'Dieser Rollenname {{name}} wird bereits verwendet.',
scope_exists: 'Die Scope-ID {{scopeId}} wurde bereits zu dieser Rolle hinzugefügt.',
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: 'Die Benutzer-ID {{userId}} wurde bereits zu dieser Rolle hinzugefügt.',
application_exists:
'Die Anwendungs-ID {{applicationId}} wurde bereits zu dieser Rolle hinzugefügt.',

View file

@ -1,6 +1,8 @@
const role = {
name_in_use: 'This role name {{name}} is already in use',
scope_exists: 'The scope id {{scopeId}} has already been added to this role',
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: 'The user id {{userId}} is already been added to this role',
application_exists: 'The application id {{applicationId}} is already been added to this role',
default_role_missing:

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: 'Este nombre de rol {{name}} ya está en uso',
scope_exists: 'El id de alcance {{scopeId}} ya ha sido agregado a este rol',
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: 'El id de usuario {{userId}} ya ha sido agregado a este rol',
application_exists: 'El id de aplicación {{applicationId}} ya ha sido agregado a este rol',
default_role_missing:

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: 'Ce nom de rôle {{name}} est déjà utilisé',
scope_exists: "L'identifiant de portée {{scopeId}} a déjà été ajouté à ce rôle",
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: "L'identifiant d'utilisateur {{userId}} a déjà été ajouté à ce rôle",
application_exists: "L'identifiant d'application {{applicationId}} a déjà été ajouté à ce rôle",
default_role_missing:

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: 'Il nome di ruolo {{name}} è già in uso',
scope_exists: "L'identificatore di ambito {{scopeId}} è già stato aggiunto a questo ruolo",
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: "L'identificatore di utente {{userId}} è già stato aggiunto a questo ruolo",
application_exists:
"L'ID dell'applicazione {{applicationId}} è già stato aggiunto a questo ruolo",

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: 'このロール名{{name}}はすでに使用されています',
scope_exists: 'スコープID {{scopeId}}はすでにこのロールに追加されています',
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: 'ユーザーID{{userId}}はすでにこのロールに追加されています',
application_exists: 'アプリケーション ID {{applicationId}} はすでにこのロールに追加されています',
default_role_missing:

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: '역할 이름 {{name}}이/가 이미 사용 중이에요.',
scope_exists: '범위 ID {{scopeId}}이/가 이미 이 역할에 추가되어 있어요.',
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: '사용자 ID {{userId}}이/가 이미 이 역할에 추가되어 있어요.',
application_exists: '애플리케이션 ID {{applicationId}} 가 이미 이 역할에 추가되어 있어요.',
default_role_missing:

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: 'Ta nazwa roli {{name}} jest już w użyciu',
scope_exists: 'Identyfikator zakresu {{scopeId}} został już dodany do tej roli',
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: 'Identyfikator użytkownika {{userId}} został już dodany do tej roli',
application_exists: 'Identyfikator aplikacji {{applicationId}} został już dodany do tej roli',
default_role_missing:

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: 'Este nome de papel {{name}} já está em uso',
scope_exists: 'O id de escopo {{scopeId}} já foi adicionado a este papel',
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: 'O id de usuário {{userId}} já foi adicionado a este papel',
application_exists: 'O id do aplicativo {{applicationId}} já foi adicionado a este papel',
default_role_missing:

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: 'Este nome de função {{name}} já está em uso',
scope_exists: 'O id do escopo {{scopeId}} já foi adicionado a esta função',
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: 'O id do usuário {{userId}} já foi adicionado a esta função',
application_exists: 'O id de aplicação {{applicationId}} já foi adicionado a esta função',
default_role_missing:

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: 'Это имя роли {{name}} уже используется',
scope_exists: 'Идентификатор области действия {{scopeId}} уже был добавлен в эту роль',
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: 'Идентификатор пользователя {{userId}} уже был добавлен в эту роль',
application_exists: 'Идентификатор приложения {{applicationId}} уже был добавлен в эту роль',
default_role_missing:

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: 'Bu rol adı {{name}} zaten kullanımda',
scope_exists: 'Bu kapsam kimliği {{scopeId}} zaten bu role eklendi',
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: 'Bu kullanıcı kimliği {{userId}} zaten bu role eklendi',
application_exists: 'Bu uygulama kimliği {{applicationId}} zaten bu role eklendi',
default_role_missing:

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: '此角色名称 {{name}} 已被使用',
scope_exists: '作用域 ID {{scopeId}} 已添加到此角色',
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: '用户 ID {{userId}} 已添加到此角色',
application_exists: '应用程序 ID {{applicationId}} 已添加到此角色',
default_role_missing: '某些默认角色名称在数据库中不存在,请确保先创建角色',

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: '此角色名稱 {{name}} 已被使用',
scope_exists: '作用域 ID {{scopeId}} 已添加到此角色',
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: '用戶 ID {{userId}} 已添加到此角色',
application_exists: '應用程式 ID {{applicationId}} 已添加到此角色',
default_role_missing: '某些默認角色名稱在數據庫中不存在,請確保先創建角色',

View file

@ -1,6 +1,9 @@
const role = {
name_in_use: '此角色名稱 {{name}} 已被使用',
scope_exists: '作用域 ID {{scopeId}} 已添加到此角色',
/** UNTRANSLATED */
management_api_scopes_not_assignable_to_user_role:
'Cannot assign management API scopes to a user role.',
user_exists: '用戶 ID {{userId}} 已添加到此角色',
application_exists: '已經將應用程式 ID {{applicationId}} 添加到此角色',
default_role_missing: '某些預設角色名稱在資料庫中不存在,請確保先創建角色',

View file

@ -0,0 +1,47 @@
import { sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
enum RoleType {
User = 'User',
}
const getManagementApiResourceIndicator = (tenantId: string) => `https://${tenantId}.logto.app/api`;
// Remove management API scopes assigned to user roles, in case they were assigned by management API and bypassed the constraints in admin console.
const alteration: AlterationScript = {
up: async (pool) => {
const { rows } = await pool.query<{
rolesScopesId: string;
indicator: string;
tenantId: string;
}>(sql`
select
roles_scopes.id as "rolesScopesId",
roles_scopes.tenant_id as "tenantId",
resources.indicator as indicator from roles_scopes
join roles
on roles_scopes.role_id = roles.id and roles_scopes.tenant_id = roles.tenant_id
join scopes on
roles_scopes.scope_id = scopes.id and roles_scopes.tenant_id = scopes.tenant_id
join resources on
scopes.resource_id = resources.id and scopes.tenant_id = resources.tenant_id
where roles.type = ${RoleType.User};
`);
const rolesScopesIdsToRemove = rows
.filter(
({ indicator, tenantId }) => indicator === getManagementApiResourceIndicator(tenantId)
)
.map(({ rolesScopesId }) => rolesScopesId);
if (rolesScopesIdsToRemove.length > 0) {
await pool.query(sql`
delete from roles_scopes where id in (${sql.join(rolesScopesIdsToRemove, sql`, `)});
`);
}
},
down: async (pool) => {
// It cannot be reverted automatically.
},
};
export default alteration;