mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): filter scopes assigned to organization roles
This commit is contained in:
parent
726a65dd8e
commit
3c2cc3c6ef
7 changed files with 111 additions and 14 deletions
7
.changeset/afraid-buckets-accept.md
Normal file
7
.changeset/afraid-buckets-accept.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"@logto/core": minor
|
||||
---
|
||||
|
||||
filter scopes assigned to organization roles
|
||||
|
||||
For api `GET /resources`, extend the query parameter `includeScopes` with new value `assignedByOrganizations` to filter scopes assigned by organization roles only.
|
|
@ -45,8 +45,8 @@ class OrganizationRolesQueries extends SchemaQueries<
|
|||
}
|
||||
|
||||
override async findAll(
|
||||
limit: number,
|
||||
offset: number,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
search?: SearchOptions<OrganizationRoleKeys>
|
||||
): Promise<[totalNumber: number, rows: Readonly<OrganizationRoleWithScopes[]>]> {
|
||||
return Promise.all([
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
{
|
||||
"in": "query",
|
||||
"name": "includeScopes",
|
||||
"description": "If it's provided with a truthy value (`true`, `1`, `yes`), the scopes of each resource will be included in the response."
|
||||
"description": "If it's provided with a truthy value (`true`, `1`, `yes`) or `assignedByOrganizations`, the scopes of each resource will be included in the response. And if it is set to `assignedByOrganizations`, only the scopes assigned by organizations will be included."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
|
|
@ -3,6 +3,7 @@ import { generateStandardId } from '@logto/shared';
|
|||
import { yes } from '@silverhand/essentials';
|
||||
import { boolean, object, string } from 'zod';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
|
@ -33,6 +34,7 @@ export default function resourceRoutes<T extends ManagementApiRouter>(
|
|||
deleteResourceById,
|
||||
},
|
||||
scopes: scopeQueries,
|
||||
organizations: { roles: organizationRolesQueries },
|
||||
} = queries;
|
||||
|
||||
router.get(
|
||||
|
@ -48,13 +50,22 @@ export default function resourceRoutes<T extends ManagementApiRouter>(
|
|||
async (ctx, next) => {
|
||||
const { limit, offset, disabled } = ctx.pagination;
|
||||
const {
|
||||
query: { includeScopes },
|
||||
query: { includeScopes: includeScopesQuery },
|
||||
} = ctx.guard;
|
||||
|
||||
const includeOnlyOrganizationScopes = includeScopesQuery === 'assignedByOrganizations';
|
||||
const includeScopes = includeOnlyOrganizationScopes || yes(includeScopesQuery);
|
||||
|
||||
const [, organizationRoles] =
|
||||
includeOnlyOrganizationScopes && EnvSet.values.isDevFeaturesEnabled
|
||||
? await organizationRolesQueries.findAll()
|
||||
: [undefined, undefined];
|
||||
|
||||
if (disabled) {
|
||||
const resources = await findAllResources();
|
||||
ctx.body = yes(includeScopes)
|
||||
? await attachScopesToResources(resources, scopeQueries)
|
||||
|
||||
ctx.body = includeScopes
|
||||
? await attachScopesToResources(resources, scopeQueries, organizationRoles)
|
||||
: resources;
|
||||
|
||||
return next();
|
||||
|
@ -66,8 +77,8 @@ export default function resourceRoutes<T extends ManagementApiRouter>(
|
|||
]);
|
||||
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = yes(includeScopes)
|
||||
? await attachScopesToResources(resources, scopeQueries)
|
||||
ctx.body = includeScopes
|
||||
? await attachScopesToResources(resources, scopeQueries, organizationRoles)
|
||||
: resources;
|
||||
|
||||
return next();
|
||||
|
|
|
@ -1,10 +1,23 @@
|
|||
import { type Resource, type ResourceResponse } from '@logto/schemas';
|
||||
import {
|
||||
type OrganizationRoleWithScopes,
|
||||
type Resource,
|
||||
type ResourceResponse,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
|
||||
/**
|
||||
* Query and attach scopes to resources.
|
||||
*
|
||||
* @param resources list of resources
|
||||
* @param scopeQueries queries
|
||||
* @param organizationRoles when provided, only the scopes that are assigned to the organization roles
|
||||
* @returns
|
||||
*/
|
||||
export const attachScopesToResources = async (
|
||||
resources: readonly Resource[],
|
||||
scopeQueries: Queries['scopes']
|
||||
scopeQueries: Queries['scopes'],
|
||||
organizationRoles?: readonly OrganizationRoleWithScopes[]
|
||||
): Promise<ResourceResponse[]> => {
|
||||
const { findScopesByResourceIds } = scopeQueries;
|
||||
const resourceIds = resources.map(({ id }) => id);
|
||||
|
@ -12,6 +25,14 @@ export const attachScopesToResources = async (
|
|||
|
||||
return resources.map((resource) => ({
|
||||
...resource,
|
||||
scopes: scopes.filter(({ resourceId }) => resourceId === resource.id),
|
||||
scopes: scopes
|
||||
.filter(({ resourceId }) => resourceId === resource.id)
|
||||
.filter(
|
||||
({ id }) =>
|
||||
!organizationRoles ||
|
||||
organizationRoles.some(({ resourceScopes }) =>
|
||||
resourceScopes.some((scope) => scope.id === id)
|
||||
)
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { Resource, CreateResource } from '@logto/schemas';
|
||||
import type { Resource, CreateResource, Scope } from '@logto/schemas';
|
||||
import { conditionalString } from '@silverhand/essentials';
|
||||
import { type Options } from 'ky';
|
||||
|
||||
import { generateResourceIndicator, generateResourceName } from '#src/utils.js';
|
||||
|
@ -15,7 +16,14 @@ export const createResource = async (name?: string, indicator?: string) =>
|
|||
})
|
||||
.json<Resource>();
|
||||
|
||||
export const getResources = async () => authedAdminApi.get('resources').json<Resource[]>();
|
||||
export const getResources = async (query?: string) =>
|
||||
authedAdminApi.get(`resources${conditionalString(query && `?${query}`)}`).json<
|
||||
Array<
|
||||
Resource & {
|
||||
scopes?: Scope[];
|
||||
}
|
||||
>
|
||||
>();
|
||||
|
||||
export const getResource = async (resourceId: string, options?: Options) =>
|
||||
authedAdminApi.get(`resources/${resourceId}`, options).json<Resource>();
|
||||
|
|
|
@ -9,8 +9,15 @@ import {
|
|||
deleteResource,
|
||||
setDefaultResource,
|
||||
} from '#src/api/index.js';
|
||||
import { createScope } from '#src/api/scope.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { generateResourceIndicator, generateResourceName } from '#src/utils.js';
|
||||
import { OrganizationRoleApiTest } from '#src/helpers/organization.js';
|
||||
import {
|
||||
generateResourceIndicator,
|
||||
generateResourceName,
|
||||
generateRoleName,
|
||||
generateScopeName,
|
||||
} from '#src/utils.js';
|
||||
|
||||
describe('admin console api resources', () => {
|
||||
it('should get management api resource details successfully', async () => {
|
||||
|
@ -67,6 +74,49 @@ describe('admin console api resources', () => {
|
|||
expect(resources.findIndex(({ name }) => name === resourceName)).not.toBe(-1);
|
||||
});
|
||||
|
||||
it('should get resource list with scopes successfully', async () => {
|
||||
const resourceName = generateResourceName();
|
||||
const resourceIndicator = generateResourceIndicator();
|
||||
|
||||
const resource = await createResource(resourceName, resourceIndicator);
|
||||
const scopeName = generateScopeName();
|
||||
await createScope(resource.id, scopeName);
|
||||
|
||||
// Get all resources
|
||||
const resources = await getResources('includeScopes=true');
|
||||
|
||||
expect(resources.find(({ name }) => name === resourceName)).toHaveProperty('scopes');
|
||||
});
|
||||
|
||||
it('should get resource list with organization assigned scopes successfully', async () => {
|
||||
const roleApi = new OrganizationRoleApiTest();
|
||||
|
||||
const resourceName = generateResourceName();
|
||||
const resourceIndicator = generateResourceIndicator();
|
||||
|
||||
const resource = await createResource(resourceName, resourceIndicator);
|
||||
const scopeName = generateScopeName();
|
||||
const scopeName2 = generateScopeName();
|
||||
const scope = await createScope(resource.id, scopeName);
|
||||
await createScope(resource.id, scopeName2);
|
||||
|
||||
await roleApi.create({
|
||||
name: generateRoleName(),
|
||||
description: 'test description.',
|
||||
resourceScopeIds: [scope.id],
|
||||
});
|
||||
|
||||
const resourcesWithAllScopes = await getResources('includeScopes=true');
|
||||
expect(resourcesWithAllScopes.find(({ name }) => name === resourceName)?.scopes).toHaveLength(
|
||||
2
|
||||
);
|
||||
|
||||
const resources = await getResources('includeScopes=assignedByOrganizations');
|
||||
expect(resources.find(({ name }) => name === resourceName)?.scopes).toHaveLength(1);
|
||||
|
||||
await roleApi.cleanUp();
|
||||
});
|
||||
|
||||
it('should update api resource details successfully', async () => {
|
||||
const resource = await createResource();
|
||||
|
||||
|
|
Loading…
Reference in a new issue