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:
parent
2a99ba5997
commit
873d20fed9
25 changed files with 237 additions and 56 deletions
64
packages/core/src/libraries/role-scope.ts
Normal file
64
packages/core/src/libraries/role-scope.ts
Normal 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,
|
||||
};
|
||||
};
|
|
@ -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 },
|
||||
]);
|
||||
|
|
|
@ -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 }))
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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 }))
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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: '某些默认角色名称在数据库中不存在,请确保先创建角色',
|
||||
|
|
|
@ -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: '某些默認角色名稱在數據庫中不存在,請確保先創建角色',
|
||||
|
|
|
@ -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: '某些預設角色名稱在資料庫中不存在,請確保先創建角色',
|
||||
|
|
|
@ -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;
|
Loading…
Reference in a new issue