From 5adf3dfad795d3a0a06dbb32cfeb7bf50b5ba223 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Tue, 30 Apr 2024 15:09:13 +0800 Subject: [PATCH] feat(core,schemas): add CRUD for consent organization resource scopes (#5804) feat(core,schemas): add crud for user consent organization resource scopes --- .../constants.ts | 5 ++ .../use-application-scopes-assignment.ts | 2 + packages/core/src/libraries/application.ts | 53 +++++++++++++--- .../application-user-consent-scopes.ts | 10 +++ packages/core/src/queries/application.ts | 3 + ...pplication-user-consent-scope.openapi.json | 6 ++ .../application-user-consent-scope.ts | 44 +++++++++---- .../src/api/application-user-consent-scope.ts | 1 + .../application-user-consent-scope.test.ts | 62 +++++++++++++++++++ packages/schemas/src/types/application.ts | 16 +++-- 10 files changed, 176 insertions(+), 26 deletions(-) diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/ApplicationScopesAssignmentModal/constants.ts b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/ApplicationScopesAssignmentModal/constants.ts index 87854264f..b9cc89f6f 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/ApplicationScopesAssignmentModal/constants.ts +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/ApplicationScopesAssignmentModal/constants.ts @@ -10,6 +10,11 @@ export const permissionTabs = Object.freeze({ title: 'application_details.permissions.api_resource', key: ApplicationUserConsentScopeType.ResourceScopes, }, + [ApplicationUserConsentScopeType.OrganizationResourceScopes]: { + // TODO @xiaoyijun: update the title + title: 'application_details.permissions.api_resource', + key: ApplicationUserConsentScopeType.OrganizationResourceScopes, + }, [ApplicationUserConsentScopeType.OrganizationScopes]: { title: 'application_details.permissions.organization', key: ApplicationUserConsentScopeType.OrganizationScopes, diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/ApplicationScopesAssignmentModal/use-application-scopes-assignment.ts b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/ApplicationScopesAssignmentModal/use-application-scopes-assignment.ts index 516aa1008..6f32c9dfa 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/ApplicationScopesAssignmentModal/use-application-scopes-assignment.ts +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/ApplicationScopesAssignmentModal/use-application-scopes-assignment.ts @@ -72,6 +72,8 @@ const useApplicationScopesAssignment = (applicationId: string) => { [ApplicationUserConsentScopeType.UserScopes]: userScopesAssignment, [ApplicationUserConsentScopeType.OrganizationScopes]: organizationScopesAssignment, [ApplicationUserConsentScopeType.ResourceScopes]: resourceScopesAssignment, + // TODO @xiaoyijun: Replace with correct scopes + [ApplicationUserConsentScopeType.OrganizationResourceScopes]: resourceScopesAssignment, }), [organizationScopesAssignment, resourceScopesAssignment, userScopesAssignment] ); diff --git a/packages/core/src/libraries/application.ts b/packages/core/src/libraries/application.ts index e58cdbad0..048cb0a65 100644 --- a/packages/core/src/libraries/application.ts +++ b/packages/core/src/libraries/application.ts @@ -32,6 +32,7 @@ export const createApplicationLibrary = (queries: Queries) => { applications: { findApplicationById, userConsentOrganizationScopes, + userConsentOrganizationResourceScopes, userConsentResourceScopes, userConsentUserScopes, }, @@ -76,16 +77,20 @@ export const createApplicationLibrary = (queries: Queries) => { { organizationScopes = [], resourceScopes = [], + organizationResourceScopes = [], }: { organizationScopes?: string[]; resourceScopes?: string[]; + organizationResourceScopes?: string[]; }, tenantId: string ) => { - const [organizationScopesData, resourceScopesData] = await Promise.all([ - organizationScopesQuery.findByIds(organizationScopes), - findScopesByIds(resourceScopes), - ]); + const [organizationScopesData, resourceScopesData, organizationResourceScopesData] = + await Promise.all([ + organizationScopesQuery.findByIds(organizationScopes), + findScopesByIds(resourceScopes), + findScopesByIds(organizationResourceScopes), + ]); const invalidOrganizationScopes = organizationScopes.filter( (scope) => !organizationScopesData.some(({ id }) => id === scope) @@ -95,22 +100,28 @@ export const createApplicationLibrary = (queries: Queries) => { (scope) => !resourceScopesData.some(({ id }) => id === scope) ); + const invalidOrganizationResourceScopes = organizationResourceScopes.filter( + (scope) => !organizationResourceScopesData.some(({ id }) => id === scope) + ); + // Assert that all scopes exist, return the missing ones assertThat( - invalidOrganizationScopes.length === 0 && invalidResourceScopes.length === 0, + invalidOrganizationScopes.length === 0 && + invalidResourceScopes.length === 0 && + invalidOrganizationResourceScopes.length === 0, new RequestError( { code: 'application.user_consent_scopes_not_found', status: 422, }, - { invalidOrganizationScopes, invalidResourceScopes } + { invalidOrganizationScopes, invalidResourceScopes, invalidOrganizationResourceScopes } ) ); const managementApiResourceIndicator = getManagementApiResourceIndicator(tenantId); const managementApiScopes = await findScopesByIdsAndResourceIndicator( - resourceScopes, + [...resourceScopes, ...organizationResourceScopes], managementApiResourceIndicator ); @@ -129,10 +140,12 @@ export const createApplicationLibrary = (queries: Queries) => { { organizationScopes, resourceScopes, + organizationResourceScopes, userScopes, }: { organizationScopes?: string[]; resourceScopes?: string[]; + organizationResourceScopes?: string[]; userScopes?: string[]; } ) => { @@ -148,6 +161,12 @@ export const createApplicationLibrary = (queries: Queries) => { ); } + if (organizationResourceScopes) { + await userConsentOrganizationResourceScopes.insert( + ...organizationResourceScopes.map<[string, string]>((scope) => [applicationId, scope]) + ); + } + if (userScopes) { await Promise.all( userScopes.map(async (userScope) => @@ -181,6 +200,21 @@ export const createApplicationLibrary = (queries: Queries) => { ); }; + const getApplicationUserConsentOrganizationResourceScopes = async (applicationId: string) => { + const [, scopes] = await userConsentOrganizationResourceScopes.getEntities(Scopes, { + applicationId, + }); + + const groupedScopes = groupResourceScopesByResourceId(scopes); + + return Promise.all( + groupedScopes.map(async ({ resourceId, scopes }) => ({ + resource: await findResourceById(resourceId), + scopes, + })) + ); + }; + const getApplicationUserConsentScopes = async (applicationId: string) => userConsentUserScopes.findAllByApplicationId(applicationId); @@ -198,6 +232,10 @@ export const createApplicationLibrary = (queries: Queries) => { await userConsentResourceScopes.delete({ applicationId, scopeId }); break; } + case ApplicationUserConsentScopeType.OrganizationResourceScopes: { + await userConsentOrganizationResourceScopes.delete({ applicationId, scopeId }); + break; + } case ApplicationUserConsentScopeType.UserScopes: { await userConsentUserScopes.deleteByApplicationIdAndScopeId(applicationId, scopeId); break; @@ -250,6 +288,7 @@ export const createApplicationLibrary = (queries: Queries) => { assignApplicationUserConsentScopes, getApplicationUserConsentOrganizationScopes, getApplicationUserConsentResourceScopes, + getApplicationUserConsentOrganizationResourceScopes, getApplicationUserConsentScopes, deleteApplicationUserConsentScopesByTypeAndScopeId, validateUserConsentOrganizationMembership, diff --git a/packages/core/src/queries/application-user-consent-scopes.ts b/packages/core/src/queries/application-user-consent-scopes.ts index 394be6c71..e03e0c4f3 100644 --- a/packages/core/src/queries/application-user-consent-scopes.ts +++ b/packages/core/src/queries/application-user-consent-scopes.ts @@ -2,6 +2,7 @@ import { type UserScope } from '@logto/core-kit'; import { ApplicationUserConsentOrganizationScopes, ApplicationUserConsentResourceScopes, + ApplicationUserConsentOrganizationResourceScopes, ApplicationUserConsentUserScopes, Applications, OrganizationScopes, @@ -32,6 +33,15 @@ export class ApplicationUserConsentResourceScopeQueries extends TwoRelationsQuer } } +export class ApplicationUserConsentOrganizationResourceScopeQueries extends TwoRelationsQueries< + typeof Applications, + typeof Scopes +> { + constructor(pool: CommonQueryMethods) { + super(pool, ApplicationUserConsentOrganizationResourceScopes.table, Applications, Scopes); + } +} + export const createApplicationUserConsentUserScopeQueries = (pool: CommonQueryMethods) => { const insert = buildInsertIntoWithPool(pool)(ApplicationUserConsentUserScopes, { onConflict: { ignore: true }, diff --git a/packages/core/src/queries/application.ts b/packages/core/src/queries/application.ts index 31f7dbf42..d09153883 100644 --- a/packages/core/src/queries/application.ts +++ b/packages/core/src/queries/application.ts @@ -15,6 +15,7 @@ import type { OmitAutoSetFields } from '#src/utils/sql.js'; import ApplicationUserConsentOrganizationsQuery from './application-user-consent-organizations.js'; import { + ApplicationUserConsentOrganizationResourceScopeQueries, ApplicationUserConsentOrganizationScopeQueries, ApplicationUserConsentResourceScopeQueries, createApplicationUserConsentUserScopeQueries, @@ -253,6 +254,8 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => { deleteApplicationById, userConsentOrganizationScopes: new ApplicationUserConsentOrganizationScopeQueries(pool), userConsentResourceScopes: new ApplicationUserConsentResourceScopeQueries(pool), + userConsentOrganizationResourceScopes: + new ApplicationUserConsentOrganizationResourceScopeQueries(pool), userConsentUserScopes: createApplicationUserConsentUserScopeQueries(pool), userConsentOrganizations: new ApplicationUserConsentOrganizationsQuery(pool), }; diff --git a/packages/core/src/routes/applications/application-user-consent-scope.openapi.json b/packages/core/src/routes/applications/application-user-consent-scope.openapi.json index a0de2d9c6..7ca52a7db 100644 --- a/packages/core/src/routes/applications/application-user-consent-scope.openapi.json +++ b/packages/core/src/routes/applications/application-user-consent-scope.openapi.json @@ -15,6 +15,9 @@ "resourceScopes": { "description": "A list of resource scope id to assign to the application. Throws error if any given resource scope is not found." }, + "organizationResourceScopes": { + "description": "A list of organization resource scope id to assign to the application. Throws error if any given resource scope is not found." + }, "userScopes": { "description": "A list of user scope enum value to assign to the application." } @@ -51,6 +54,9 @@ "resourceScopes": { "description": "A list of resource scope details grouped by resource id assigned to the application." }, + "organizationResourceScopes": { + "description": "A list of organization resource scope details grouped by resource id assigned to the application." + }, "userScopes": { "description": "A list of user scope enum value assigned to the application." } diff --git a/packages/core/src/routes/applications/application-user-consent-scope.ts b/packages/core/src/routes/applications/application-user-consent-scope.ts index cbb11a89a..22ee94c55 100644 --- a/packages/core/src/routes/applications/application-user-consent-scope.ts +++ b/packages/core/src/routes/applications/application-user-consent-scope.ts @@ -5,6 +5,7 @@ import { } from '@logto/schemas'; import { object, string, nativeEnum } from 'zod'; +import { EnvSet } from '#src/env-set/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import type { ManagementApiRouter, RouterInitArgs } from '../types.js'; @@ -24,6 +25,7 @@ export default function applicationUserConsentScopeRoutes { @@ -77,17 +86,26 @@ export default function applicationUserConsentScopeRoutes authedAdminApi.post(`applications/${applicationId}/user-consent-scopes`, { json: payload }); diff --git a/packages/integration-tests/src/tests/api/application/application-user-consent-scope.test.ts b/packages/integration-tests/src/tests/api/application/application-user-consent-scope.test.ts index cf5744936..fb44d4ada 100644 --- a/packages/integration-tests/src/tests/api/application/application-user-consent-scope.test.ts +++ b/packages/integration-tests/src/tests/api/application/application-user-consent-scope.test.ts @@ -16,6 +16,7 @@ describe('assign user consent scopes to application', () => { const applicationIds = new Map(); const organizationScopes = new Map(); const resourceScopes = new Map(); + const organizationResourceScopes = new Map(); const resourceIds = new Set(); const organizationScopeApi = new OrganizationScopeApi(); @@ -52,6 +53,12 @@ describe('assign user consent scopes to application', () => { resourceScopes.set('resourceScope1', resourceScope1.id); resourceScopes.set('resourceScope2', resourceScope2.id); + + const resourceScope3 = await createScope(resource.id); + const resourceScope4 = await createScope(resource.id); + + organizationResourceScopes.set('resourceScope1', resourceScope3.id); + organizationResourceScopes.set('resourceScope2', resourceScope4.id); }); afterAll(async () => { @@ -75,6 +82,7 @@ describe('assign user consent scopes to application', () => { assignUserConsentScopes(applicationIds.get('firstPartyApp')!, { organizationScopes: Array.from(organizationScopes.values()), resourceScopes: Array.from(resourceScopes.values()), + organizationResourceScopes: Array.from(organizationResourceScopes.values()), }), { code: 'application.third_party_application_only', @@ -107,11 +115,24 @@ describe('assign user consent scopes to application', () => { ); }); + it('should throw error when trying to assign a non-existing organization resource scope', async () => { + await expectRejects( + assignUserConsentScopes(applicationIds.get('thirdPartyApp')!, { + organizationResourceScopes: ['non-existing-resource-scope'], + }), + { + code: 'application.user_consent_scopes_not_found', + status: 422, + } + ); + }); + it('should assign scopes to third-party application successfully', async () => { await expect( assignUserConsentScopes(applicationIds.get('thirdPartyApp')!, { organizationScopes: Array.from(organizationScopes.values()), resourceScopes: Array.from(resourceScopes.values()), + organizationResourceScopes: Array.from(organizationResourceScopes.values()), userScopes: [UserScope.Profile, UserScope.Email, UserScope.OrganizationRoles], }) ).resolves.not.toThrow(); @@ -122,6 +143,7 @@ describe('assign user consent scopes to application', () => { assignUserConsentScopes(applicationIds.get('thirdPartyApp')!, { organizationScopes: [organizationScopes.get('organizationScope1')!], resourceScopes: [resourceScopes.get('resourceScope1')!], + organizationResourceScopes: [organizationResourceScopes.get('resourceScope1')!], userScopes: [UserScope.Profile], }) ).resolves.not.toThrow(); @@ -154,6 +176,18 @@ describe('assign user consent scopes to application', () => { ).toBeTruthy(); } + expect(result.organizationResourceScopes.length).toBe(1); + expect(result.organizationResourceScopes[0]!.resource.id).toBe(Array.from(resourceIds)[0]); + expect(result.organizationResourceScopes[0]!.scopes.length).toBe( + organizationResourceScopes.size + ); + + for (const resourceScopeId of organizationResourceScopes.values()) { + expect( + result.organizationResourceScopes[0]!.scopes.some(({ id }) => id === resourceScopeId) + ).toBeTruthy(); + } + expect(result.userScopes.length).toBe(3); for (const userScope of [UserScope.Profile, UserScope.Email, UserScope.OrganizationRoles]) { @@ -170,6 +204,7 @@ describe('assign user consent scopes to application', () => { expect(result.organizationScopes.length).toBe(0); expect(result.resourceScopes.length).toBe(0); + expect(result.organizationResourceScopes.length).toBe(0); expect(result.userScopes.length).toBe(0); await deleteApplication(newApp.id); @@ -214,6 +249,18 @@ describe('assign user consent scopes to application', () => { } ); + await expectRejects( + deleteUserConsentScopes( + applicationIds.get('thirdPartyApp')!, + ApplicationUserConsentScopeType.OrganizationResourceScopes, + 'non-existing-resource-scope' + ), + { + code: 'entity.not_found', + status: 404, + } + ); + await expectRejects( deleteUserConsentScopes( applicationIds.get('thirdPartyApp')!, @@ -232,6 +279,7 @@ describe('assign user consent scopes to application', () => { assignUserConsentScopes(applicationIds.get('thirdPartyApp')!, { organizationScopes: Array.from(organizationScopes.values()), resourceScopes: Array.from(resourceScopes.values()), + organizationResourceScopes: Array.from(organizationResourceScopes.values()), userScopes: [UserScope.Profile, UserScope.Email, UserScope.OrganizationRoles], }) ).resolves.not.toThrow(); @@ -252,6 +300,14 @@ describe('assign user consent scopes to application', () => { ) ).resolves.not.toThrow(); + await expect( + deleteUserConsentScopes( + applicationIds.get('thirdPartyApp')!, + ApplicationUserConsentScopeType.OrganizationResourceScopes, + organizationResourceScopes.get('resourceScope1')! + ) + ).resolves.not.toThrow(); + await expect( deleteUserConsentScopes( applicationIds.get('thirdPartyApp')!, @@ -274,6 +330,12 @@ describe('assign user consent scopes to application', () => { ) ).toBeUndefined(); + expect( + result.organizationResourceScopes[0]!.scopes.find( + ({ id }) => id === organizationResourceScopes.get('resourceScope1')! + ) + ).toBeUndefined(); + expect(result.userScopes.includes(UserScope.OrganizationRoles)).toBeFalsy(); }); }); diff --git a/packages/schemas/src/types/application.ts b/packages/schemas/src/types/application.ts index 138dab7b1..4ffcd20f9 100644 --- a/packages/schemas/src/types/application.ts +++ b/packages/schemas/src/types/application.ts @@ -40,22 +40,26 @@ export const applicationPatchGuard = applicationCreateGuard.partial().omit({ isThirdParty: true, }); +const resourceScopesGuard = z.array( + z.object({ + resource: Resources.guard.pick({ id: true, name: true, indicator: true }), + scopes: z.array(Scopes.guard.pick({ id: true, name: true, description: true })), + }) +); + export const applicationUserConsentScopesResponseGuard = z.object({ organizationScopes: z.array( OrganizationScopes.guard.pick({ id: true, name: true, description: true }) ), - resourceScopes: z.array( - z.object({ - resource: Resources.guard.pick({ id: true, name: true, indicator: true }), - scopes: z.array(Scopes.guard.pick({ id: true, name: true, description: true })), - }) - ), + resourceScopes: resourceScopesGuard, + organizationResourceScopes: resourceScopesGuard, userScopes: z.array(z.nativeEnum(UserScope)), }); export enum ApplicationUserConsentScopeType { OrganizationScopes = 'organization-scopes', ResourceScopes = 'resource-scopes', + OrganizationResourceScopes = 'organization-resource-scopes', UserScopes = 'user-scopes', }