0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

feat(core,schemas): add GET application user consent scopes api (#5103)

* feat(core,schemas): add GET application user consent scopes api

add GET application user consent scopes api

* fix(core): remove useless field declare

remove useless field declare
This commit is contained in:
simeng-li 2023-12-15 18:12:52 +08:00 committed by GitHub
parent cb43ebb7d1
commit 5c936e0976
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 223 additions and 6 deletions

View file

@ -1,9 +1,25 @@
import type { Scope } from '@logto/schemas';
import { OrganizationScopes, Scopes, type Scope } 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';
const groupResourceScopesByResourceId = (
scopes: readonly Scope[]
): Array<{
resourceId: string;
scopes: Scope[];
}> => {
const resourceMap = new Map<string, Scope[]>();
for (const scope of scopes) {
const existingScopes = resourceMap.get(scope.resourceId) ?? [];
resourceMap.set(scope.resourceId, [...existingScopes, scope]);
}
return Array.from(resourceMap, ([resourceId, scopes]) => ({ resourceId, scopes }));
};
export const createApplicationLibrary = (queries: Queries) => {
const {
applications: {
@ -16,6 +32,7 @@ export const createApplicationLibrary = (queries: Queries) => {
rolesScopes: { findRolesScopesByRoleIds },
organizations: { scopes: organizationScopesQuery },
scopes: { findScopesByIdsAndResourceIndicator, findScopesByIds },
resources: { findResourceById },
} = queries;
const findApplicationScopesForResourceIndicator = async (
@ -112,10 +129,40 @@ export const createApplicationLibrary = (queries: Queries) => {
}
};
// Get application user consent organization scopes
const getApplicationUserConsentOrganizationScopes = async (applicationId: string) => {
const [, scopes] = await userConsentOrganizationScopes.getEntities(OrganizationScopes, {
applicationId,
});
return scopes;
};
const getApplicationUserConsentResourceScopes = async (applicationId: string) => {
const [, scopes] = await userConsentResourceScopes.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) =>
useConsentUserScopes.findAllByApplicationId(applicationId);
return {
validateThirdPartyApplicationById,
findApplicationScopesForResourceIndicator,
validateApplicationUserConsentScopes,
assignApplicationUserConsentScopes,
getApplicationUserConsentOrganizationScopes,
getApplicationUserConsentResourceScopes,
getApplicationUserConsentScopes,
};
};

View file

@ -1,3 +1,4 @@
import { type UserScope } from '@logto/core-kit';
import {
ApplicationUserConsentOrganizationScopes,
ApplicationUserConsentResourceScopes,
@ -6,7 +7,8 @@ import {
OrganizationScopes,
Scopes,
} from '@logto/schemas';
import { type CommonQueryMethods } from 'slonik';
import { convertToIdentifiers } from '@logto/shared';
import { sql, type CommonQueryMethods } from 'slonik';
import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
import { TwoRelationsQueries } from '#src/utils/RelationQueries.js';
@ -34,7 +36,20 @@ export const createApplicationUserConsentUserScopeQueries = (pool: CommonQueryMe
onConflict: { ignore: true },
});
const findAllByApplicationId = async (applicationId: string) => {
const { table, fields } = convertToIdentifiers(ApplicationUserConsentUserScopes);
const scopes = await pool.any<{ userScope: UserScope }>(sql`
select ${fields.userScope}
from ${table}
where ${fields.applicationId} = ${applicationId}
`);
return scopes.map(({ userScope }) => userScope);
};
return {
insert,
findAllByApplicationId,
};
};

View file

@ -34,6 +34,35 @@
"description": "Any of the given organization scope, resource scope or user scope is not found"
}
}
},
"get": {
"summary": "List all the user consent scopes of an application.",
"description": "List all the user consent scopes of an application by application id",
"responses": {
"200": {
"description": "All the user consent scopes of the application are listed successfully",
"content": {
"application/json": {
"schema": {
"properties": {
"organizationScopes": {
"description": "A list of organization scope details assigned to the application."
},
"resourceScopes": {
"description": "A list of resource scope details grouped by resource id assigned to the application."
},
"userScopes": {
"description": "A list of user scope enum value assigned to the application."
}
}
}
}
}
},
"404": {
"description": "The application is not found"
}
}
}
}
}

View file

@ -1,4 +1,5 @@
import { UserScope } from '@logto/core-kit';
import { applicationUserConsentScopesResponseGuard } from '@logto/schemas';
import { object, string, nativeEnum } from 'zod';
import koaGuard from '#src/middleware/koa-guard.js';
@ -9,11 +10,17 @@ export default function applicationUserConsentScopeRoutes<T extends AuthedRouter
...[
router,
{
queries: {
applications: { findApplicationById },
},
libraries: {
applications: {
validateThirdPartyApplicationById,
validateApplicationUserConsentScopes,
assignApplicationUserConsentScopes,
getApplicationUserConsentOrganizationScopes,
getApplicationUserConsentResourceScopes,
getApplicationUserConsentScopes,
},
},
},
@ -49,4 +56,35 @@ export default function applicationUserConsentScopeRoutes<T extends AuthedRouter
return next();
}
);
router.get(
'/applications/:applicationId/user-consent-scopes',
koaGuard({
params: object({
applicationId: string(),
}),
response: applicationUserConsentScopesResponseGuard,
status: [200, 404],
}),
async (ctx, next) => {
const { applicationId } = ctx.guard.params;
await findApplicationById(applicationId);
// Note: The following queries will return full data schema, we rely on the response guard to filter out the fields we don't need.
const [organizationScopes, resourceScopes, userScopes] = await Promise.all([
getApplicationUserConsentOrganizationScopes(applicationId),
getApplicationUserConsentResourceScopes(applicationId),
getApplicationUserConsentScopes(applicationId),
]);
ctx.body = {
organizationScopes,
resourceScopes,
userScopes,
};
return next();
}
);
}

View file

@ -19,7 +19,14 @@ type TableInfo<
guard: z.ZodType<Schema, z.ZodTypeDef, unknown>;
};
type InferSchema<T> = T extends TableInfo<infer _, infer _, infer _, infer Schema> ? Schema : never;
type InferSchema<T> = T extends TableInfo<
infer _,
infer _,
Extract<keyof (infer Schema), string>,
infer Schema
>
? Schema
: never;
type CamelCaseIdObject<T extends string> = KeysToCamelCase<{
[Key in `${T}_id`]: string;

View file

@ -1,4 +1,5 @@
import { type UserScope } from '@logto/core-kit';
import { type ApplicationUserConsentScopesResponse } from '@logto/schemas';
import { authedAdminApi } from './api.js';
@ -10,3 +11,8 @@ export const assignUserConsentScopes = async (
userScopes?: UserScope[];
}
) => authedAdminApi.post(`applications/${applicationId}/user-consent-scopes`, { json: payload });
export const getUserConsentScopes = async (applicationId: string) =>
authedAdminApi
.get(`applications/${applicationId}/user-consent-scopes`)
.json<ApplicationUserConsentScopesResponse>();

View file

@ -1,7 +1,10 @@
import { UserScope } from '@logto/core-kit';
import { ApplicationType } from '@logto/schemas';
import { assignUserConsentScopes } from '#src/api/application-user-consent-scope.js';
import {
assignUserConsentScopes,
getUserConsentScopes,
} from '#src/api/application-user-consent-scope.js';
import { createApplication, deleteApplication } from '#src/api/application.js';
import { OrganizationScopeApi } from '#src/api/organization-scope.js';
import { createResource, deleteResource } from '#src/api/resource.js';
@ -122,4 +125,52 @@ describe('assign user consent scopes to application', () => {
})
).resolves.not.toThrow();
});
it('should return 404 when trying to get consent scopes from non-existing application', async () => {
await expectRejects(getUserConsentScopes('non-existing-application'), {
code: 'entity.not_exists_with_id',
statusCode: 404,
});
});
it('should return consent scopes successfully', async () => {
// This test depends on the previous success assignment test
const result = await getUserConsentScopes(applicationIds.get('thirdPartyApp')!);
expect(result.organizationScopes.length).toBe(organizationScopes.size);
for (const organizationScopeId of organizationScopes.values()) {
expect(result.organizationScopes.some(({ id }) => id === organizationScopeId)).toBeTruthy();
}
expect(result.resourceScopes.length).toBe(1);
expect(result.resourceScopes[0]!.resource.id).toBe(Array.from(resourceIds)[0]);
expect(result.resourceScopes[0]!.scopes.length).toBe(resourceScopes.size);
for (const resourceScopeId of resourceScopes.values()) {
expect(
result.resourceScopes[0]!.scopes.some(({ id }) => id === resourceScopeId)
).toBeTruthy();
}
expect(result.userScopes.length).toBe(3);
for (const userScope of [UserScope.Profile, UserScope.Email, UserScope.OrganizationRoles]) {
expect(result.userScopes.includes(userScope)).toBeTruthy();
}
});
it('should return empty consent scopes when no scopes assigned', async () => {
const newApp = await createApplication('new-app', ApplicationType.Traditional, {
isThirdParty: true,
});
const result = await getUserConsentScopes(newApp.id);
expect(result.organizationScopes.length).toBe(0);
expect(result.resourceScopes.length).toBe(0);
expect(result.userScopes.length).toBe(0);
await deleteApplication(newApp.id);
});
});

View file

@ -1,6 +1,13 @@
import { type z } from 'zod';
import { UserScope } from '@logto/core-kit';
import { z } from 'zod';
import { Applications, type Application } from '../db-entries/index.js';
import {
Applications,
type Application,
OrganizationScopes,
Resources,
Scopes,
} from '../db-entries/index.js';
export type ApplicationResponse = Application & { isAdmin: boolean };
@ -31,3 +38,20 @@ export const applicationPatchGuard = applicationCreateGuard.partial().omit({
type: true,
isThirdParty: 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 })),
})
),
userScopes: z.array(z.nativeEnum(UserScope)),
});
export type ApplicationUserConsentScopesResponse = z.infer<
typeof applicationUserConsentScopesResponseGuard
>;