mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
fix(core): not allow to modify management api resource (#5626)
This commit is contained in:
parent
be57ec2e19
commit
5b03030de2
22 changed files with 161 additions and 21 deletions
9
.changeset/four-goats-rush.md
Normal file
9
.changeset/four-goats-rush.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
"@logto/integration-tests": patch
|
||||
"@logto/phrases": patch
|
||||
"@logto/core": patch
|
||||
---
|
||||
|
||||
Not allow to modify management API resource through API.
|
||||
|
||||
Previously, management API resource and its scopes are readonly in Console. But it was possible to modify through the API. This is not allowed anymore.
|
|
@ -1,4 +1,8 @@
|
|||
import { type Resource, type CreateResource } from '@logto/schemas';
|
||||
import {
|
||||
type Resource,
|
||||
type CreateResource,
|
||||
getManagementApiResourceIndicator,
|
||||
} from '@logto/schemas';
|
||||
import { pickDefault } from '@logto/shared/esm';
|
||||
import { type Nullable } from '@silverhand/essentials';
|
||||
|
||||
|
@ -91,6 +95,19 @@ describe('resource scope routes', () => {
|
|||
expect(response.status).toEqual(400);
|
||||
});
|
||||
|
||||
it('POST /resources/:id/scopes should throw when the resource is management API', async () => {
|
||||
const { findResourceById } = resources;
|
||||
findResourceById.mockResolvedValueOnce({
|
||||
...mockResource,
|
||||
indicator: getManagementApiResourceIndicator('mock'),
|
||||
});
|
||||
await expect(
|
||||
resourceScopeRequest
|
||||
.post('/resources/foo/scopes')
|
||||
.send({ name: 'name', description: 'description' })
|
||||
).resolves.toHaveProperty('status', 400);
|
||||
});
|
||||
|
||||
it('PATCH /resources/:id/scopes/:scopeId', async () => {
|
||||
const name = 'write:users';
|
||||
const description = 'description';
|
||||
|
@ -107,10 +124,35 @@ describe('resource scope routes', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('PATCH /resources/:id/scopes/:scopeId should throw when the resource is management API', async () => {
|
||||
const { findResourceById } = resources;
|
||||
findResourceById.mockResolvedValueOnce({
|
||||
...mockResource,
|
||||
indicator: getManagementApiResourceIndicator('mock'),
|
||||
});
|
||||
await expect(
|
||||
resourceScopeRequest
|
||||
.patch('/resources/foo/scopes/foz')
|
||||
.send({ name: 'name', description: 'description' })
|
||||
).resolves.toHaveProperty('status', 400);
|
||||
});
|
||||
|
||||
it('DELETE /resources/:id/scopes/:scopeId', async () => {
|
||||
await expect(resourceScopeRequest.delete('/resources/foo/scopes/foz')).resolves.toHaveProperty(
|
||||
'status',
|
||||
204
|
||||
);
|
||||
});
|
||||
|
||||
it('DELETE /resources/:id/scopes/:scopeId should throw when the resource is management API', async () => {
|
||||
const { findResourceById } = resources;
|
||||
findResourceById.mockResolvedValueOnce({
|
||||
...mockResource,
|
||||
indicator: getManagementApiResourceIndicator('mock'),
|
||||
});
|
||||
await expect(resourceScopeRequest.delete('/resources/foo/scopes/foz')).resolves.toHaveProperty(
|
||||
'status',
|
||||
400
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Scopes } from '@logto/schemas';
|
||||
import { Scopes, isManagementApi } from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { tryThat } from '@silverhand/essentials';
|
||||
import { object, string } from 'zod';
|
||||
|
@ -93,15 +93,10 @@ export default function resourceScopeRoutes<T extends AuthedRouter>(
|
|||
|
||||
assertThat(!/\s/.test(body.name), 'scope.name_with_space');
|
||||
|
||||
const { indicator } = await findResourceById(resourceId);
|
||||
assertThat(
|
||||
await findResourceById(resourceId),
|
||||
new RequestError({
|
||||
code: 'entity.not_exists_with_id',
|
||||
name: 'resource',
|
||||
id: resourceId,
|
||||
resourceId,
|
||||
status: 404,
|
||||
})
|
||||
!isManagementApi(indicator),
|
||||
new RequestError({ code: 'resource.cannot_modify_management_api' })
|
||||
);
|
||||
|
||||
assertThat(
|
||||
|
@ -138,15 +133,10 @@ export default function resourceScopeRoutes<T extends AuthedRouter>(
|
|||
body,
|
||||
} = ctx.guard;
|
||||
|
||||
const { indicator } = await findResourceById(resourceId);
|
||||
assertThat(
|
||||
await findResourceById(resourceId),
|
||||
new RequestError({
|
||||
code: 'entity.not_exists_with_id',
|
||||
name: 'resource',
|
||||
id: resourceId,
|
||||
resourceId,
|
||||
status: 404,
|
||||
})
|
||||
!isManagementApi(indicator),
|
||||
new RequestError({ code: 'resource.cannot_modify_management_api' })
|
||||
);
|
||||
|
||||
if (body.name) {
|
||||
|
@ -171,13 +161,19 @@ export default function resourceScopeRoutes<T extends AuthedRouter>(
|
|||
'/resources/:resourceId/scopes/:scopeId',
|
||||
koaGuard({
|
||||
params: object({ resourceId: string().min(1), scopeId: string().min(1) }),
|
||||
status: [204, 404],
|
||||
status: [204, 400, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { scopeId },
|
||||
params: { scopeId, resourceId },
|
||||
} = ctx.guard;
|
||||
|
||||
const { indicator } = await findResourceById(resourceId);
|
||||
assertThat(
|
||||
!isManagementApi(indicator),
|
||||
new RequestError({ code: 'resource.cannot_modify_management_api' })
|
||||
);
|
||||
|
||||
await deleteScopeById(scopeId);
|
||||
|
||||
ctx.status = 204;
|
||||
|
|
|
@ -152,6 +152,15 @@ describe('resource routes', () => {
|
|||
expect(response.status).toEqual(400);
|
||||
});
|
||||
|
||||
it('PATCH /resources/:id should throw when trying to modify management API', async () => {
|
||||
const { findResourceById } = resources;
|
||||
findResourceById.mockResolvedValueOnce({
|
||||
...mockResource,
|
||||
indicator: getManagementApiResourceIndicator('mock'),
|
||||
});
|
||||
await expect(resourceRequest.patch('/resources/foo')).resolves.toHaveProperty('status', 400);
|
||||
});
|
||||
|
||||
it('DELETE /resources/:id', async () => {
|
||||
await expect(resourceRequest.delete('/resources/foo')).resolves.toHaveProperty('status', 204);
|
||||
});
|
||||
|
|
|
@ -136,7 +136,7 @@ export default function resourceRoutes<T extends AuthedRouter>(
|
|||
// Use the dedicated API `PATCH /resources/:id/is-default` to update.
|
||||
body: Resources.createGuard.omit({ id: true, indicator: true, isDefault: true }).partial(),
|
||||
response: Resources.guard,
|
||||
status: [200, 404],
|
||||
status: [200, 400, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
|
@ -144,6 +144,12 @@ export default function resourceRoutes<T extends AuthedRouter>(
|
|||
body,
|
||||
} = ctx.guard;
|
||||
|
||||
const { indicator } = await findResourceById(id);
|
||||
assertThat(
|
||||
!isManagementApi(indicator),
|
||||
new RequestError({ code: 'resource.cannot_modify_management_api' })
|
||||
);
|
||||
|
||||
const resource = await updateResourceById(id, body);
|
||||
ctx.body = resource;
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { HTTPError } from 'ky';
|
|||
|
||||
import { createResource } from '#src/api/index.js';
|
||||
import { createScope, deleteScope, getScopes, updateScope } from '#src/api/scope.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { generateName, generateScopeName } from '#src/utils.js';
|
||||
|
||||
describe('scopes', () => {
|
||||
|
@ -56,6 +57,13 @@ describe('scopes', () => {
|
|||
expect(response instanceof HTTPError && response.response.status === 400).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw when creating scope under management API resource', async () => {
|
||||
await expectRejects(createScope(defaultManagementApi.resource.id, 'scope'), {
|
||||
code: 'resource.cannot_modify_management_api',
|
||||
status: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update scope successfully', async () => {
|
||||
const resource = await createResource();
|
||||
const scope = await createScope(resource.id);
|
||||
|
@ -114,6 +122,23 @@ describe('scopes', () => {
|
|||
expect(response instanceof HTTPError && response.response.status === 404).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw when updating scope under management API resource', async () => {
|
||||
const scopes = await getScopes(defaultManagementApi.resource.id);
|
||||
const scopeId = scopes[0]?.id;
|
||||
if (!scopeId) {
|
||||
throw new Error('No scope for management API resource found');
|
||||
}
|
||||
await expectRejects(
|
||||
updateScope(defaultManagementApi.resource.id, scopeId, {
|
||||
name: 'scope',
|
||||
}),
|
||||
{
|
||||
code: 'resource.cannot_modify_management_api',
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete scope successfully', async () => {
|
||||
const resource = await createResource();
|
||||
const scope = await createScope(resource.id);
|
||||
|
@ -126,4 +151,16 @@ describe('scopes', () => {
|
|||
|
||||
expect(scopes.some(({ name }) => name === scope.name)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should throw when deleting scope under management API resource', async () => {
|
||||
const scopes = await getScopes(defaultManagementApi.resource.id);
|
||||
const scopeId = scopes[0]?.id;
|
||||
if (!scopeId) {
|
||||
throw new Error('No scope for management API resource found');
|
||||
}
|
||||
await expectRejects(deleteScope(defaultManagementApi.resource.id, scopeId), {
|
||||
code: 'resource.cannot_modify_management_api',
|
||||
status: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -96,6 +96,18 @@ describe('admin console api resources', () => {
|
|||
expect(response instanceof HTTPError && response.response.status === 400).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw when updating management api resource', async () => {
|
||||
await expectRejects(
|
||||
updateResource(defaultManagementApi.resource.id, {
|
||||
name: 'new-name',
|
||||
}),
|
||||
{
|
||||
code: 'resource.cannot_modify_management_api',
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should not update api resource indicator', async () => {
|
||||
const resource = await createResource();
|
||||
const newResourceName = `new_${resource.name}`;
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: 'Die API-Kennung {{indicator}} wird bereits verwendet',
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const resource = {
|
||||
resource_identifier_in_use: 'The API identifier {{indicator}} is already in use',
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: '"El identificador de API {{indicator}} ya está en uso',
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: "L'identifiant d'API {{indicator}} est déjà utilisé",
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: "L'identificatore API {{indicator}} è già in uso",
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: 'API識別子 {{indicator}} はすでに使用されています',
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: 'API 식별자 {{indicator}}가 이미 사용 중입니다',
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: 'Identyfikator API {{indicator}} jest już używany',
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: 'O identificador de API {{indicator}} já está em uso',
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: 'O identificador da API {{indicator}} já está em uso',
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: 'Идентификатор API {{indicator}} уже используется',
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: 'API tanımlayıcısı {{indicator}} zaten kullanımda',
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: 'API 标识符 {{indicator}} 已被使用',
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: 'API 識別碼 {{indicator}} 已經被使用',
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
|
@ -2,6 +2,8 @@ const resource = {
|
|||
resource_identifier_in_use: 'API 識別碼 {{indicator}} 已經被使用',
|
||||
/** UNTRANSLATED */
|
||||
cannot_delete_management_api: 'Cannot delete Logto management API.',
|
||||
/** UNTRANSLATED */
|
||||
cannot_modify_management_api: 'Cannot modify Logto management API.',
|
||||
};
|
||||
|
||||
export default Object.freeze(resource);
|
||||
|
|
Loading…
Add table
Reference in a new issue