Skip to content

Commit 73c7c28

Browse files
authored
feat(core): apply system limit for organization related resources (#7942)
1 parent ab28e68 commit 73c7c28

File tree

4 files changed

+127
-9
lines changed

4 files changed

+127
-9
lines changed

packages/core/src/routes/organization-role/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
OrganizationRoles,
33
organizationRoleWithScopesGuard,
44
ProductEvent,
5+
RoleType,
56
type CreateOrganizationRole,
67
type OrganizationRole,
78
type OrganizationRoleKeys,
@@ -35,6 +36,7 @@ export default function organizationRoleRoutes<T extends ManagementApiRouter>(
3536
relations: { rolesScopes, rolesResourceScopes },
3637
},
3738
},
39+
libraries: { quota },
3840
},
3941
]: RouterInitArgs<T>
4042
) {
@@ -95,10 +97,17 @@ export default function organizationRoleRoutes<T extends ManagementApiRouter>(
9597
koaGuard({
9698
body: createGuard,
9799
response: OrganizationRoles.guard,
98-
status: [201, 422],
100+
status: [201, 422, 403],
99101
}),
100102
async (ctx, next) => {
101103
const { organizationScopeIds, resourceScopeIds, ...data } = ctx.guard.body;
104+
105+
await quota.guardTenantUsageByKey(
106+
data.type === RoleType.MachineToMachine
107+
? 'organizationMachineToMachineRolesLimit'
108+
: 'organizationUserRolesLimit'
109+
);
110+
102111
const role = await roles.insert({ id: generateStandardId(), ...data });
103112

104113
if (organizationScopeIds.length > 0) {

packages/core/src/routes/organization-scope/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { OrganizationScopes } from '@logto/schemas';
22

33
import SchemaRouter from '#src/utils/SchemaRouter.js';
44

5+
import { koaQuotaGuard } from '../../middleware/koa-quota-guard.js';
56
import { errorHandler } from '../organization/utils.js';
67
import { type ManagementApiRouter, type RouterInitArgs } from '../types.js';
78

@@ -12,11 +13,19 @@ export default function organizationScopeRoutes<T extends ManagementApiRouter>(
1213
queries: {
1314
organizations: { scopes },
1415
},
16+
libraries: { quota },
1517
},
1618
]: RouterInitArgs<T>
1719
) {
1820
const router = new SchemaRouter(OrganizationScopes, scopes, {
19-
middlewares: [],
21+
middlewares: [
22+
{
23+
middleware: koaQuotaGuard({ key: 'organizationScopesLimit', quota }),
24+
scope: 'native',
25+
method: ['post'],
26+
status: [403],
27+
},
28+
],
2029
errorHandler,
2130
searchFields: ['name'],
2231
isPaginationOptional: true,

packages/core/src/routes/organization/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export default function organizationRoutes<T extends ManagementApiRouter>(
107107
}
108108
);
109109

110-
userRoutes(router, organizations);
110+
userRoutes(router, organizations, quota);
111111
applicationRoutes(router, organizations);
112112
jitRoutes(router, organizations);
113113

packages/core/src/routes/organization/user/index.ts

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import {
33
type CreateOrganization,
44
type Organization,
55
userWithOrganizationRolesGuard,
6+
Users,
67
} from '@logto/schemas';
78
import { z } from 'zod';
89

10+
import { buildManagementApiContext } from '#src/libraries/hook/utils.js';
11+
import { type QuotaLibrary } from '#src/libraries/quota.js';
912
import koaGuard from '#src/middleware/koa-guard.js';
1013
import koaPagination from '#src/middleware/koa-pagination.js';
1114
import type OrganizationQueries from '#src/queries/organization/index.js';
@@ -18,13 +21,9 @@ import userRoleRelationRoutes from './role-relations.js';
1821
/** Mounts the user-related routes on the organization router. */
1922
export default function userRoutes(
2023
router: SchemaRouter<OrganizationKeys, CreateOrganization, Organization>,
21-
organizations: OrganizationQueries
24+
organizations: OrganizationQueries,
25+
quota: QuotaLibrary
2226
) {
23-
router.addRelationRoutes(organizations.relations.users, undefined, {
24-
disabled: { get: true },
25-
hookEvent: 'Organization.Membership.Updated',
26-
});
27-
2827
router.get(
2928
'/:id/users',
3029
koaPagination(),
@@ -50,6 +49,107 @@ export default function userRoutes(
5049
}
5150
);
5251

52+
router.post(
53+
'/:id/users',
54+
koaGuard({
55+
params: z.object({ id: z.string().min(1) }),
56+
body: z.object({ userIds: z.string().min(1).array().nonempty() }),
57+
status: [201, 403, 422],
58+
}),
59+
async (ctx, next) => {
60+
const {
61+
params: { id },
62+
body: { userIds },
63+
} = ctx.guard;
64+
65+
// Check quota limit before adding users
66+
await quota.guardTenantUsageByKey('usersPerOrganizationLimit', {
67+
entityId: id,
68+
consumeUsageCount: userIds.length,
69+
});
70+
71+
await organizations.relations.users.insert(
72+
...userIds.map((userId) => ({ organizationId: id, userId }))
73+
);
74+
75+
ctx.status = 201;
76+
77+
// Trigger hook event
78+
ctx.appendDataHookContext('Organization.Membership.Updated', {
79+
...buildManagementApiContext(ctx),
80+
organizationId: id,
81+
});
82+
83+
return next();
84+
}
85+
);
86+
87+
router.put(
88+
'/:id/users',
89+
koaGuard({
90+
params: z.object({ id: z.string().min(1) }),
91+
body: z.object({ userIds: z.string().min(1).array() }),
92+
status: [204, 403, 422],
93+
}),
94+
async (ctx, next) => {
95+
const {
96+
params: { id },
97+
body: { userIds },
98+
} = ctx.guard;
99+
100+
// For replace operation, calculate the delta (new count - current count)
101+
// Only check quota if we're adding more users than currently exist
102+
const [currentCount] = await organizations.relations.users.getEntities(Users, {
103+
organizationId: id,
104+
});
105+
const delta = userIds.length - currentCount;
106+
107+
if (delta > 0) {
108+
await quota.guardTenantUsageByKey('usersPerOrganizationLimit', {
109+
entityId: id,
110+
consumeUsageCount: delta,
111+
});
112+
}
113+
114+
await organizations.relations.users.replace(id, userIds);
115+
116+
ctx.status = 204;
117+
118+
// Trigger hook event
119+
ctx.appendDataHookContext('Organization.Membership.Updated', {
120+
...buildManagementApiContext(ctx),
121+
organizationId: id,
122+
});
123+
124+
return next();
125+
}
126+
);
127+
128+
router.delete(
129+
'/:id/users/:userId',
130+
koaGuard({
131+
params: z.object({ id: z.string().min(1), userId: z.string().min(1) }),
132+
status: [204, 422],
133+
}),
134+
async (ctx, next) => {
135+
const {
136+
params: { id, userId },
137+
} = ctx.guard;
138+
139+
await organizations.relations.users.delete({ organizationId: id, userId });
140+
141+
ctx.status = 204;
142+
143+
// Trigger hook event
144+
ctx.appendDataHookContext('Organization.Membership.Updated', {
145+
...buildManagementApiContext(ctx),
146+
organizationId: id,
147+
});
148+
149+
return next();
150+
}
151+
);
152+
53153
router.post(
54154
'/:id/users/roles',
55155
koaGuard({

0 commit comments

Comments
 (0)