@logto/schemas 1.9.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ import { type CommonQueryMethods, sql } from 'slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const getDatabaseName = async (pool: CommonQueryMethods) => {
6
+ const { currentDatabase } = await pool.one<{ currentDatabase: string }>(sql`
7
+ select current_database();
8
+ `);
9
+
10
+ return currentDatabase.replaceAll('-', '_');
11
+ };
12
+
13
+ const alteration: AlterationScript = {
14
+ up: async (pool) => {
15
+ const database = await getDatabaseName(pool);
16
+ const baseRoleId = sql.identifier([`logto_tenant_${database}`]);
17
+
18
+ await pool.query(sql`
19
+ create type sentinel_action_result as enum ('Success', 'Failed');
20
+
21
+ create type sentinel_decision as enum ('Undecided', 'Allowed', 'Blocked', 'Challenge');
22
+
23
+ create table sentinel_activities (
24
+ tenant_id varchar(21) not null
25
+ references tenants (id) on update cascade on delete cascade,
26
+ id varchar(21) not null,
27
+ /** The target that the action was performed on. */
28
+ target_type varchar(32) /* @use SentinelActivityTargetType */ not null,
29
+ /** The target hashed identifier. */
30
+ target_hash varchar(64) not null,
31
+ /** The action name that was performed. */
32
+ action varchar(64) /* @use SentinelActivityAction */ not null,
33
+ /** If the action was successful or not. */
34
+ action_result sentinel_action_result not null,
35
+ /** Additional payload data if any. */
36
+ payload jsonb /* @use SentinelActivityPayload */ not null,
37
+ /** The sentinel decision for the action. */
38
+ decision sentinel_decision not null,
39
+ /** The expiry date of the decision. */
40
+ decision_expires_at timestamptz not null default(now()),
41
+ /** The time the activity was created. */
42
+ created_at timestamptz not null default(now()),
43
+ primary key (id)
44
+ );
45
+
46
+ create index sentinel_activities__id
47
+ on sentinel_activities (tenant_id, id);
48
+
49
+ create index sentinel_activities__target_type_target_hash_action_action_result_decision
50
+ on sentinel_activities (tenant_id, target_type, target_hash, action, action_result, decision);
51
+
52
+ create trigger set_tenant_id before insert on sentinel_activities
53
+ for each row execute procedure set_tenant_id();
54
+
55
+ alter table sentinel_activities enable row level security;
56
+
57
+ create policy sentinel_activities_tenant_id on sentinel_activities
58
+ as restrictive
59
+ using (tenant_id = (select id from tenants where db_user = current_user));
60
+
61
+ create policy sentinel_activities_modification on sentinel_activities
62
+ using (true);
63
+
64
+ grant select, insert, update, delete on sentinel_activities to ${baseRoleId};
65
+ `);
66
+ },
67
+ down: async (pool) => {
68
+ await pool.query(sql`
69
+ drop policy sentinel_activities_tenant_id on sentinel_activities;
70
+ drop policy sentinel_activities_modification on sentinel_activities;
71
+
72
+ drop table sentinel_activities;
73
+ drop type sentinel_action_result;
74
+ drop type sentinel_decision;
75
+ `);
76
+ },
77
+ };
78
+
79
+ export default alteration;
@@ -0,0 +1,307 @@
1
+ import { generateStandardId } from '@logto/shared/universal';
2
+ import { deduplicate } from '@silverhand/essentials';
3
+ import { sql } from 'slonik';
4
+
5
+ import type { AlterationScript } from '../lib/types/alteration.js';
6
+
7
+ enum InternalRole {
8
+ Admin = '#internal:admin',
9
+ }
10
+
11
+ enum RoleType {
12
+ User = 'User',
13
+ MachineToMachine = 'MachineToMachine',
14
+ }
15
+
16
+ enum PredefinedScope {
17
+ All = 'all',
18
+ }
19
+
20
+ const getManagementApiResourceIndicator = (tenantId: string) => `https://${tenantId}.logto.app/api`;
21
+
22
+ const managementApiAccessRoleName = 'Management API access';
23
+ const managementApiAccessRoleDescription = 'Management API access';
24
+
25
+ const alteration: AlterationScript = {
26
+ up: async (pool) => {
27
+ /**
28
+ * Step 1
29
+ * Get all internal admin roles.
30
+ */
31
+ /**
32
+ * Notice that in our case:
33
+ * Each tenant has only one built-in management API resource and one attached internal admin role.
34
+ * Each internal admin role has only one scope (`PredefinedScope.All`).
35
+ *
36
+ * Can go to @logto/schemas/src/{utils,seeds}/* to find more details.
37
+ *
38
+ * Based on this setup, we can use the following query to get all internal admin roles.
39
+ */
40
+ const { rows: internalManagementApiRolesCandidates } = await pool.query<{
41
+ roleId: string;
42
+ tenantId: string;
43
+ scopeId: string;
44
+ indicator: string;
45
+ }>(sql`
46
+ select
47
+ roles.id as "role_id",
48
+ roles.tenant_id as "tenant_id",
49
+ scopes.id as "scope_id",
50
+ resources.indicator as "indicator" from roles
51
+ join roles_scopes
52
+ on roles_scopes.role_id = roles.id and roles_scopes.tenant_id = roles.tenant_id
53
+ join scopes
54
+ on scopes.id = roles_scopes.scope_id and scopes.tenant_id = roles_scopes.tenant_id
55
+ join resources
56
+ on resources.id = scopes.resource_id and resources.tenant_id = scopes.tenant_id
57
+ where
58
+ roles.name = ${InternalRole.Admin}
59
+ and roles.type = ${RoleType.MachineToMachine}
60
+ and scopes.name = ${PredefinedScope.All}
61
+ and resources.indicator like ${getManagementApiResourceIndicator('%')}
62
+ and resources.name = 'Logto Management API'
63
+ `);
64
+ // Can not directly use the result from the query unless we use subquery, separate the filter and subquery for easy understanding.
65
+ const internalManagementApiRoles = internalManagementApiRolesCandidates.filter(
66
+ ({ indicator, tenantId }) => indicator === getManagementApiResourceIndicator(tenantId)
67
+ );
68
+ /**
69
+ * Step 2
70
+ * Get all applications_roles related to the internal admin roles.
71
+ */
72
+ const { rows: applicationRoles } = await pool.query<{
73
+ id: string;
74
+ applicationId: string;
75
+ roleId: string;
76
+ tenantId: string;
77
+ }>(sql`
78
+ select * from applications_roles where (role_id, tenant_id) in (values ${sql.join(
79
+ internalManagementApiRoles.map(({ roleId, tenantId }) => sql`( ${roleId}, ${tenantId} )`),
80
+ sql`, `
81
+ )});
82
+ `);
83
+ /**
84
+ * Step 3
85
+ * Create new roles with only management API access for tenants (m2m apps with internal admin access should share the same role),
86
+ * we can not directly assign internal admin roles to m2m applications since it's "internal" and invisible to Logto users.
87
+ */
88
+ /** A tenant can have multiple applications with internal admin role, hence need to `deduplicate()`. */
89
+ const tenantsNeedManagementApiAccessRole = deduplicate(
90
+ applicationRoles.map(({ tenantId }) => tenantId)
91
+ );
92
+ if (tenantsNeedManagementApiAccessRole.length === 0) {
93
+ return;
94
+ }
95
+ const { rows: insertedRoles } = await pool.query<{
96
+ id: string;
97
+ tenantId: string;
98
+ name: string;
99
+ description: string;
100
+ type: RoleType;
101
+ }>(sql`
102
+ insert into roles (tenant_id, id, name, description, type) values ${sql.join(
103
+ tenantsNeedManagementApiAccessRole.map(
104
+ (tenantId) =>
105
+ sql`( ${tenantId}, ${generateStandardId()}, ${managementApiAccessRoleName}, ${managementApiAccessRoleDescription}, ${
106
+ RoleType.MachineToMachine
107
+ } )`
108
+ ),
109
+ sql`, `
110
+ )} returning *;
111
+ `);
112
+ /**
113
+ * Step 4
114
+ * Assign internal admin access scopes to new roles.
115
+ */
116
+ await Promise.all(
117
+ insertedRoles.map(async ({ tenantId, id: roleId }) => {
118
+ const internalRoleForTenant = internalManagementApiRoles.find(
119
+ ({ tenantId: roleTenantId }) => tenantId === roleTenantId
120
+ );
121
+ if (!internalRoleForTenant) {
122
+ return;
123
+ }
124
+ await pool.query<{
125
+ tenantId: string;
126
+ id: string;
127
+ roleId: string;
128
+ scopeId: string;
129
+ }>(sql`
130
+ insert into roles_scopes (tenant_id, id, role_id, scope_id) values (${tenantId}, ${generateStandardId()}, ${roleId}, ${
131
+ internalRoleForTenant.scopeId
132
+ });
133
+ `);
134
+ })
135
+ );
136
+ /**
137
+ * Step 5
138
+ * Should remove internal admin access roles from m2m applications and assign new roles (created in step 3) to them.
139
+ * These two steps can be done by simply replace the role_id in applications_roles table.
140
+ */
141
+ await Promise.all(
142
+ insertedRoles.map(async ({ tenantId, id: roleId }) => {
143
+ const applicationRolesOfTheTenant = applicationRoles.filter(
144
+ ({ tenantId: applicationRoleTenantId }) => tenantId === applicationRoleTenantId
145
+ );
146
+ const previousInternalRole = internalManagementApiRoles.find(
147
+ ({ tenantId: internalRoleTenantId }) => internalRoleTenantId === tenantId
148
+ );
149
+ if (applicationRolesOfTheTenant.length === 0 || !previousInternalRole) {
150
+ return;
151
+ }
152
+ await pool.query<{
153
+ id: string;
154
+ applicationId: string;
155
+ roleId: string;
156
+ tenantId: string;
157
+ }>(sql`
158
+ update applications_roles set role_id = ${roleId} where tenant_id = ${tenantId} and role_id = ${
159
+ previousInternalRole.roleId
160
+ } and application_id in (${sql.join(
161
+ applicationRolesOfTheTenant.map(({ applicationId }) => applicationId),
162
+ sql`, `
163
+ )});
164
+ `);
165
+ })
166
+ );
167
+ },
168
+ down: async (pool) => {
169
+ /**
170
+ * Step 1
171
+ * Get all auto-created management api access roles.
172
+ * Notice that in our case: each management api access role has only one scope (`PredefinedScope.All`), and each tenant has only one management api access role.
173
+ */
174
+ /**
175
+ * Notice that in our case:
176
+ * Each tenant has only one built-in management API resource and one attached management API access role.
177
+ * Each management API access role role has only one scope (`PredefinedScope.All`).
178
+ *
179
+ * Can go to @logto/schemas/src/{utils,seeds}/* to find more details.
180
+ *
181
+ * Based on this setup, we can use the following query to get all internal admin roles.
182
+ */
183
+ const { rows: managementApiAccessRolesCandidates } = await pool.query<{
184
+ roleId: string;
185
+ tenantId: string;
186
+ scopeId: string;
187
+ indicator: string;
188
+ }>(sql`
189
+ select
190
+ roles.id as "role_id",
191
+ roles.tenant_id as "tenant_id",
192
+ scopes.id as "scope_id",
193
+ resources.indicator as "indicator" from roles
194
+ join roles_scopes
195
+ on roles_scopes.role_id = roles.id and roles_scopes.tenant_id = roles.tenant_id
196
+ join scopes
197
+ on scopes.id = roles_scopes.scope_id and scopes.tenant_id = roles_scopes.tenant_id
198
+ join resources
199
+ on resources.id = scopes.resource_id and resources.tenant_id = scopes.tenant_id
200
+ where
201
+ roles.name = ${managementApiAccessRoleName}
202
+ and roles.description = ${managementApiAccessRoleDescription}
203
+ and roles.type = ${RoleType.MachineToMachine}
204
+ and scopes.name = ${PredefinedScope.All}
205
+ and resources.indicator like ${getManagementApiResourceIndicator('%')}
206
+ and resources.name = 'Logto Management API';
207
+ `);
208
+ // Can not directly use the result from the query unless we use subquery, separate the filter and subquery for easy understanding.
209
+ const managementApiAccessRoles = managementApiAccessRolesCandidates.filter(
210
+ ({ indicator, tenantId }) => indicator === getManagementApiResourceIndicator(tenantId)
211
+ );
212
+ /**
213
+ * Step 2
214
+ * Get all applications_roles related to the management api access role.
215
+ */
216
+ if (managementApiAccessRoles.length === 0) {
217
+ return;
218
+ }
219
+ const { rows: applicationRoles } = await pool.query<{
220
+ id: string;
221
+ applicationId: string;
222
+ roleId: string;
223
+ tenantId: string;
224
+ }>(sql`
225
+ select * from applications_roles where (role_id, tenant_id) in (values ${sql.join(
226
+ managementApiAccessRoles.map(({ roleId, tenantId }) => sql`( ${roleId}, ${tenantId} )`),
227
+ sql`, `
228
+ )});
229
+ `);
230
+ /**
231
+ * Step 3
232
+ * Find all internal admin access roles.
233
+ */
234
+ const concernedTenantIds = deduplicate(
235
+ managementApiAccessRoles.map(({ tenantId }) => tenantId)
236
+ );
237
+ const { rows: internalAdminAccessRoles } = await pool.query<{
238
+ roleId: string;
239
+ tenantId: string;
240
+ }>(sql`
241
+ select
242
+ roles.id as "roleId",
243
+ roles.tenant_id as "tenantId" from roles
244
+ join roles_scopes
245
+ on roles.tenant_id = roles_scopes.tenant_id and roles.id = roles_scopes.role_id
246
+ join scopes
247
+ on scopes.tenant_id = roles_scopes.tenant_id and scopes.id = roles_scopes.scope_id
248
+ join resources
249
+ on resources.id = scopes.resource_id and resources.tenant_id = scopes.tenant_id
250
+ where
251
+ roles.name = ${InternalRole.Admin}
252
+ and ( roles.tenant_id, resources.indicator ) in (values ${sql.join(
253
+ concernedTenantIds.map(
254
+ (tenantId) => sql`( ${tenantId}, ${getManagementApiResourceIndicator(tenantId)} )`
255
+ ),
256
+ sql`, `
257
+ )});
258
+ `);
259
+ /**
260
+ * Step 4
261
+ * Assign internal admin access roles to m2m apps with management api access roles. (Found in step 2)
262
+ */
263
+ await Promise.all(
264
+ internalAdminAccessRoles.map(async ({ roleId: internalAdminAccessRoleId, tenantId }) => {
265
+ const pendingApplicationsOfTenant = applicationRoles.filter(
266
+ ({ tenantId: applicationTenantId }) => tenantId === applicationTenantId
267
+ );
268
+ const previousManagementApiAccessRole = managementApiAccessRoles.find(
269
+ ({ tenantId: managementApiAccessRoleTenantId }) =>
270
+ managementApiAccessRoleTenantId === tenantId
271
+ );
272
+ if (pendingApplicationsOfTenant.length === 0 || !previousManagementApiAccessRole) {
273
+ return;
274
+ }
275
+ await pool.query<{
276
+ id: string;
277
+ applicationId: string;
278
+ roleId: string;
279
+ tenantId: string;
280
+ }>(sql`
281
+ update applications_roles set role_id = ${internalAdminAccessRoleId} where tenant_id = ${tenantId} and role_id = ${
282
+ previousManagementApiAccessRole.roleId
283
+ } and application_id in (${sql.join(
284
+ pendingApplicationsOfTenant.map(({ applicationId }) => applicationId),
285
+ sql`, `
286
+ )});
287
+ `);
288
+ })
289
+ );
290
+ /**
291
+ * Step 5
292
+ * Remove management api access roles. (`roles_scopes` will automatically be removed if roles are removed)
293
+ */
294
+ if (managementApiAccessRoles.length === 0) {
295
+ return;
296
+ }
297
+ await Promise.all(
298
+ managementApiAccessRoles.map(async ({ roleId, tenantId }) => {
299
+ await pool.query(sql`
300
+ delete from roles where id = ${roleId} and tenant_id = ${tenantId};
301
+ `);
302
+ })
303
+ );
304
+ },
305
+ };
306
+
307
+ export default alteration;
@@ -0,0 +1,3 @@
1
+ import type { AlterationScript } from '../lib/types/alteration.js';
2
+ declare const alteration: AlterationScript;
3
+ export default alteration;
@@ -0,0 +1,72 @@
1
+ import { sql } from 'slonik';
2
+ const getDatabaseName = async (pool) => {
3
+ const { currentDatabase } = await pool.one(sql `
4
+ select current_database();
5
+ `);
6
+ return currentDatabase.replaceAll('-', '_');
7
+ };
8
+ const alteration = {
9
+ up: async (pool) => {
10
+ const database = await getDatabaseName(pool);
11
+ const baseRoleId = sql.identifier([`logto_tenant_${database}`]);
12
+ await pool.query(sql `
13
+ create type sentinel_action_result as enum ('Success', 'Failed');
14
+
15
+ create type sentinel_decision as enum ('Undecided', 'Allowed', 'Blocked', 'Challenge');
16
+
17
+ create table sentinel_activities (
18
+ tenant_id varchar(21) not null
19
+ references tenants (id) on update cascade on delete cascade,
20
+ id varchar(21) not null,
21
+ /** The target that the action was performed on. */
22
+ target_type varchar(32) /* @use SentinelActivityTargetType */ not null,
23
+ /** The target hashed identifier. */
24
+ target_hash varchar(64) not null,
25
+ /** The action name that was performed. */
26
+ action varchar(64) /* @use SentinelActivityAction */ not null,
27
+ /** If the action was successful or not. */
28
+ action_result sentinel_action_result not null,
29
+ /** Additional payload data if any. */
30
+ payload jsonb /* @use SentinelActivityPayload */ not null,
31
+ /** The sentinel decision for the action. */
32
+ decision sentinel_decision not null,
33
+ /** The expiry date of the decision. */
34
+ decision_expires_at timestamptz not null default(now()),
35
+ /** The time the activity was created. */
36
+ created_at timestamptz not null default(now()),
37
+ primary key (id)
38
+ );
39
+
40
+ create index sentinel_activities__id
41
+ on sentinel_activities (tenant_id, id);
42
+
43
+ create index sentinel_activities__target_type_target_hash_action_action_result_decision
44
+ on sentinel_activities (tenant_id, target_type, target_hash, action, action_result, decision);
45
+
46
+ create trigger set_tenant_id before insert on sentinel_activities
47
+ for each row execute procedure set_tenant_id();
48
+
49
+ alter table sentinel_activities enable row level security;
50
+
51
+ create policy sentinel_activities_tenant_id on sentinel_activities
52
+ as restrictive
53
+ using (tenant_id = (select id from tenants where db_user = current_user));
54
+
55
+ create policy sentinel_activities_modification on sentinel_activities
56
+ using (true);
57
+
58
+ grant select, insert, update, delete on sentinel_activities to ${baseRoleId};
59
+ `);
60
+ },
61
+ down: async (pool) => {
62
+ await pool.query(sql `
63
+ drop policy sentinel_activities_tenant_id on sentinel_activities;
64
+ drop policy sentinel_activities_modification on sentinel_activities;
65
+
66
+ drop table sentinel_activities;
67
+ drop type sentinel_action_result;
68
+ drop type sentinel_decision;
69
+ `);
70
+ },
71
+ };
72
+ export default alteration;
@@ -0,0 +1,3 @@
1
+ import type { AlterationScript } from '../lib/types/alteration.js';
2
+ declare const alteration: AlterationScript;
3
+ export default alteration;
@@ -0,0 +1,199 @@
1
+ import { generateStandardId } from '@logto/shared/universal';
2
+ import { deduplicate } from '@silverhand/essentials';
3
+ import { sql } from 'slonik';
4
+ var InternalRole;
5
+ (function (InternalRole) {
6
+ InternalRole["Admin"] = "#internal:admin";
7
+ })(InternalRole || (InternalRole = {}));
8
+ var RoleType;
9
+ (function (RoleType) {
10
+ RoleType["User"] = "User";
11
+ RoleType["MachineToMachine"] = "MachineToMachine";
12
+ })(RoleType || (RoleType = {}));
13
+ var PredefinedScope;
14
+ (function (PredefinedScope) {
15
+ PredefinedScope["All"] = "all";
16
+ })(PredefinedScope || (PredefinedScope = {}));
17
+ const getManagementApiResourceIndicator = (tenantId) => `https://${tenantId}.logto.app/api`;
18
+ const managementApiAccessRoleName = 'Management API access';
19
+ const managementApiAccessRoleDescription = 'Management API access';
20
+ const alteration = {
21
+ up: async (pool) => {
22
+ /**
23
+ * Step 1
24
+ * Get all internal admin roles.
25
+ */
26
+ /**
27
+ * Notice that in our case:
28
+ * Each tenant has only one built-in management API resource and one attached internal admin role.
29
+ * Each internal admin role has only one scope (`PredefinedScope.All`).
30
+ *
31
+ * Can go to @logto/schemas/src/{utils,seeds}/* to find more details.
32
+ *
33
+ * Based on this setup, we can use the following query to get all internal admin roles.
34
+ */
35
+ const { rows: internalManagementApiRolesCandidates } = await pool.query(sql `
36
+ select
37
+ roles.id as "role_id",
38
+ roles.tenant_id as "tenant_id",
39
+ scopes.id as "scope_id",
40
+ resources.indicator as "indicator" from roles
41
+ join roles_scopes
42
+ on roles_scopes.role_id = roles.id and roles_scopes.tenant_id = roles.tenant_id
43
+ join scopes
44
+ on scopes.id = roles_scopes.scope_id and scopes.tenant_id = roles_scopes.tenant_id
45
+ join resources
46
+ on resources.id = scopes.resource_id and resources.tenant_id = scopes.tenant_id
47
+ where
48
+ roles.name = ${InternalRole.Admin}
49
+ and roles.type = ${RoleType.MachineToMachine}
50
+ and scopes.name = ${PredefinedScope.All}
51
+ and resources.indicator like ${getManagementApiResourceIndicator('%')}
52
+ and resources.name = 'Logto Management API'
53
+ `);
54
+ // Can not directly use the result from the query unless we use subquery, separate the filter and subquery for easy understanding.
55
+ const internalManagementApiRoles = internalManagementApiRolesCandidates.filter(({ indicator, tenantId }) => indicator === getManagementApiResourceIndicator(tenantId));
56
+ /**
57
+ * Step 2
58
+ * Get all applications_roles related to the internal admin roles.
59
+ */
60
+ const { rows: applicationRoles } = await pool.query(sql `
61
+ select * from applications_roles where (role_id, tenant_id) in (values ${sql.join(internalManagementApiRoles.map(({ roleId, tenantId }) => sql `( ${roleId}, ${tenantId} )`), sql `, `)});
62
+ `);
63
+ /**
64
+ * Step 3
65
+ * Create new roles with only management API access for tenants (m2m apps with internal admin access should share the same role),
66
+ * we can not directly assign internal admin roles to m2m applications since it's "internal" and invisible to Logto users.
67
+ */
68
+ /** A tenant can have multiple applications with internal admin role, hence need to `deduplicate()`. */
69
+ const tenantsNeedManagementApiAccessRole = deduplicate(applicationRoles.map(({ tenantId }) => tenantId));
70
+ if (tenantsNeedManagementApiAccessRole.length === 0) {
71
+ return;
72
+ }
73
+ const { rows: insertedRoles } = await pool.query(sql `
74
+ insert into roles (tenant_id, id, name, description, type) values ${sql.join(tenantsNeedManagementApiAccessRole.map((tenantId) => sql `( ${tenantId}, ${generateStandardId()}, ${managementApiAccessRoleName}, ${managementApiAccessRoleDescription}, ${RoleType.MachineToMachine} )`), sql `, `)} returning *;
75
+ `);
76
+ /**
77
+ * Step 4
78
+ * Assign internal admin access scopes to new roles.
79
+ */
80
+ await Promise.all(insertedRoles.map(async ({ tenantId, id: roleId }) => {
81
+ const internalRoleForTenant = internalManagementApiRoles.find(({ tenantId: roleTenantId }) => tenantId === roleTenantId);
82
+ if (!internalRoleForTenant) {
83
+ return;
84
+ }
85
+ await pool.query(sql `
86
+ insert into roles_scopes (tenant_id, id, role_id, scope_id) values (${tenantId}, ${generateStandardId()}, ${roleId}, ${internalRoleForTenant.scopeId});
87
+ `);
88
+ }));
89
+ /**
90
+ * Step 5
91
+ * Should remove internal admin access roles from m2m applications and assign new roles (created in step 3) to them.
92
+ * These two steps can be done by simply replace the role_id in applications_roles table.
93
+ */
94
+ await Promise.all(insertedRoles.map(async ({ tenantId, id: roleId }) => {
95
+ const applicationRolesOfTheTenant = applicationRoles.filter(({ tenantId: applicationRoleTenantId }) => tenantId === applicationRoleTenantId);
96
+ const previousInternalRole = internalManagementApiRoles.find(({ tenantId: internalRoleTenantId }) => internalRoleTenantId === tenantId);
97
+ if (applicationRolesOfTheTenant.length === 0 || !previousInternalRole) {
98
+ return;
99
+ }
100
+ await pool.query(sql `
101
+ update applications_roles set role_id = ${roleId} where tenant_id = ${tenantId} and role_id = ${previousInternalRole.roleId} and application_id in (${sql.join(applicationRolesOfTheTenant.map(({ applicationId }) => applicationId), sql `, `)});
102
+ `);
103
+ }));
104
+ },
105
+ down: async (pool) => {
106
+ /**
107
+ * Step 1
108
+ * Get all auto-created management api access roles.
109
+ * Notice that in our case: each management api access role has only one scope (`PredefinedScope.All`), and each tenant has only one management api access role.
110
+ */
111
+ /**
112
+ * Notice that in our case:
113
+ * Each tenant has only one built-in management API resource and one attached management API access role.
114
+ * Each management API access role role has only one scope (`PredefinedScope.All`).
115
+ *
116
+ * Can go to @logto/schemas/src/{utils,seeds}/* to find more details.
117
+ *
118
+ * Based on this setup, we can use the following query to get all internal admin roles.
119
+ */
120
+ const { rows: managementApiAccessRolesCandidates } = await pool.query(sql `
121
+ select
122
+ roles.id as "role_id",
123
+ roles.tenant_id as "tenant_id",
124
+ scopes.id as "scope_id",
125
+ resources.indicator as "indicator" from roles
126
+ join roles_scopes
127
+ on roles_scopes.role_id = roles.id and roles_scopes.tenant_id = roles.tenant_id
128
+ join scopes
129
+ on scopes.id = roles_scopes.scope_id and scopes.tenant_id = roles_scopes.tenant_id
130
+ join resources
131
+ on resources.id = scopes.resource_id and resources.tenant_id = scopes.tenant_id
132
+ where
133
+ roles.name = ${managementApiAccessRoleName}
134
+ and roles.description = ${managementApiAccessRoleDescription}
135
+ and roles.type = ${RoleType.MachineToMachine}
136
+ and scopes.name = ${PredefinedScope.All}
137
+ and resources.indicator like ${getManagementApiResourceIndicator('%')}
138
+ and resources.name = 'Logto Management API';
139
+ `);
140
+ // Can not directly use the result from the query unless we use subquery, separate the filter and subquery for easy understanding.
141
+ const managementApiAccessRoles = managementApiAccessRolesCandidates.filter(({ indicator, tenantId }) => indicator === getManagementApiResourceIndicator(tenantId));
142
+ /**
143
+ * Step 2
144
+ * Get all applications_roles related to the management api access role.
145
+ */
146
+ if (managementApiAccessRoles.length === 0) {
147
+ return;
148
+ }
149
+ const { rows: applicationRoles } = await pool.query(sql `
150
+ select * from applications_roles where (role_id, tenant_id) in (values ${sql.join(managementApiAccessRoles.map(({ roleId, tenantId }) => sql `( ${roleId}, ${tenantId} )`), sql `, `)});
151
+ `);
152
+ /**
153
+ * Step 3
154
+ * Find all internal admin access roles.
155
+ */
156
+ const concernedTenantIds = deduplicate(managementApiAccessRoles.map(({ tenantId }) => tenantId));
157
+ const { rows: internalAdminAccessRoles } = await pool.query(sql `
158
+ select
159
+ roles.id as "roleId",
160
+ roles.tenant_id as "tenantId" from roles
161
+ join roles_scopes
162
+ on roles.tenant_id = roles_scopes.tenant_id and roles.id = roles_scopes.role_id
163
+ join scopes
164
+ on scopes.tenant_id = roles_scopes.tenant_id and scopes.id = roles_scopes.scope_id
165
+ join resources
166
+ on resources.id = scopes.resource_id and resources.tenant_id = scopes.tenant_id
167
+ where
168
+ roles.name = ${InternalRole.Admin}
169
+ and ( roles.tenant_id, resources.indicator ) in (values ${sql.join(concernedTenantIds.map((tenantId) => sql `( ${tenantId}, ${getManagementApiResourceIndicator(tenantId)} )`), sql `, `)});
170
+ `);
171
+ /**
172
+ * Step 4
173
+ * Assign internal admin access roles to m2m apps with management api access roles. (Found in step 2)
174
+ */
175
+ await Promise.all(internalAdminAccessRoles.map(async ({ roleId: internalAdminAccessRoleId, tenantId }) => {
176
+ const pendingApplicationsOfTenant = applicationRoles.filter(({ tenantId: applicationTenantId }) => tenantId === applicationTenantId);
177
+ const previousManagementApiAccessRole = managementApiAccessRoles.find(({ tenantId: managementApiAccessRoleTenantId }) => managementApiAccessRoleTenantId === tenantId);
178
+ if (pendingApplicationsOfTenant.length === 0 || !previousManagementApiAccessRole) {
179
+ return;
180
+ }
181
+ await pool.query(sql `
182
+ update applications_roles set role_id = ${internalAdminAccessRoleId} where tenant_id = ${tenantId} and role_id = ${previousManagementApiAccessRole.roleId} and application_id in (${sql.join(pendingApplicationsOfTenant.map(({ applicationId }) => applicationId), sql `, `)});
183
+ `);
184
+ }));
185
+ /**
186
+ * Step 5
187
+ * Remove management api access roles. (`roles_scopes` will automatically be removed if roles are removed)
188
+ */
189
+ if (managementApiAccessRoles.length === 0) {
190
+ return;
191
+ }
192
+ await Promise.all(managementApiAccessRoles.map(async ({ roleId, tenantId }) => {
193
+ await pool.query(sql `
194
+ delete from roles where id = ${roleId} and tenant_id = ${tenantId};
195
+ `);
196
+ }));
197
+ },
198
+ };
199
+ export default alteration;
@@ -8,6 +8,16 @@ export declare enum RoleType {
8
8
  User = "User",
9
9
  MachineToMachine = "MachineToMachine"
10
10
  }
11
+ export declare enum SentinelActionResult {
12
+ Success = "Success",
13
+ Failed = "Failed"
14
+ }
15
+ export declare enum SentinelDecision {
16
+ Undecided = "Undecided",
17
+ Allowed = "Allowed",
18
+ Blocked = "Blocked",
19
+ Challenge = "Challenge"
20
+ }
11
21
  export declare enum SignInMode {
12
22
  SignIn = "SignIn",
13
23
  Register = "Register",