@logto/schemas 1.9.1 → 1.9.2

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,279 @@
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 roles.id as "role_id", roles.tenant_id as "tenant_id", scopes.id as "scope_id", resources.indicator as "indicator" from roles join roles_scopes on roles_scopes.role_id = roles.id and roles_scopes.tenant_id = roles.tenant_id join scopes on scopes.id = roles_scopes.scope_id and scopes.tenant_id = roles_scopes.tenant_id join resources on resources.id = scopes.resource_id and resources.tenant_id = scopes.tenant_id
47
+ where roles.name = ${InternalRole.Admin} and roles.type = ${
48
+ RoleType.MachineToMachine
49
+ } and scopes.name = ${
50
+ PredefinedScope.All
51
+ } and resources.indicator like ${getManagementApiResourceIndicator(
52
+ '%'
53
+ )} and resources.name = 'Logto Management API'
54
+ `);
55
+ // Can not directly use the result from the query unless we use subquery, separate the filter and subquery for easy understanding.
56
+ const internalManagementApiRoles = internalManagementApiRolesCandidates.filter(
57
+ ({ indicator, tenantId }) => indicator === getManagementApiResourceIndicator(tenantId)
58
+ );
59
+ /**
60
+ * Step 2
61
+ * Get all applications_roles related to the internal admin roles.
62
+ */
63
+ const { rows: applicationRoles } = await pool.query<{
64
+ id: string;
65
+ applicationId: string;
66
+ roleId: string;
67
+ tenantId: string;
68
+ }>(sql`
69
+ select * from applications_roles where (role_id, tenant_id) in (values ${sql.join(
70
+ internalManagementApiRoles.map(({ roleId, tenantId }) => sql`( ${roleId}, ${tenantId} )`),
71
+ sql`, `
72
+ )});
73
+ `);
74
+ /**
75
+ * Step 3
76
+ * Create new roles with only management API access for tenants (m2m apps with internal admin access should share the same role),
77
+ * we can not directly assign internal admin roles to m2m applications since it's "internal" and invisible to Logto users.
78
+ */
79
+ /** A tenant can have multiple applications with internal admin role, hence need to `deduplicate()`. */
80
+ const tenantsNeedManagementApiAccessRole = deduplicate(
81
+ applicationRoles.map(({ tenantId }) => tenantId)
82
+ );
83
+ if (tenantsNeedManagementApiAccessRole.length === 0) {
84
+ return;
85
+ }
86
+ const { rows: insertedRoles } = await pool.query<{
87
+ id: string;
88
+ tenantId: string;
89
+ name: string;
90
+ description: string;
91
+ type: RoleType;
92
+ }>(sql`
93
+ insert into roles (tenant_id, id, name, description, type) values ${sql.join(
94
+ tenantsNeedManagementApiAccessRole.map(
95
+ (tenantId) =>
96
+ sql`( ${tenantId}, ${generateStandardId()}, ${managementApiAccessRoleName}, ${managementApiAccessRoleDescription}, ${
97
+ RoleType.MachineToMachine
98
+ } )`
99
+ ),
100
+ sql`, `
101
+ )} returning *;
102
+ `);
103
+ /**
104
+ * Step 4
105
+ * Assign internal admin access scopes to new roles.
106
+ */
107
+ await Promise.all(
108
+ insertedRoles.map(async ({ tenantId, id: roleId }) => {
109
+ const internalRoleForTenant = internalManagementApiRoles.find(
110
+ ({ tenantId: roleTenantId }) => tenantId === roleTenantId
111
+ );
112
+ if (!internalRoleForTenant) {
113
+ return;
114
+ }
115
+ await pool.query<{
116
+ tenantId: string;
117
+ id: string;
118
+ roleId: string;
119
+ scopeId: string;
120
+ }>(sql`
121
+ insert into roles_scopes (tenant_id, id, role_id, scope_id) values (${tenantId}, ${generateStandardId()}, ${roleId}, ${
122
+ internalRoleForTenant.scopeId
123
+ });
124
+ `);
125
+ })
126
+ );
127
+ /**
128
+ * Step 5
129
+ * Should remove internal admin access roles from m2m applications and assign new roles (created in step 3) to them.
130
+ * These two steps can be done by simply replace the role_id in applications_roles table.
131
+ */
132
+ await Promise.all(
133
+ insertedRoles.map(async ({ tenantId, id: roleId }) => {
134
+ const applicationRolesOfTheTenant = applicationRoles.filter(
135
+ ({ tenantId: applicationRoleTenantId }) => tenantId === applicationRoleTenantId
136
+ );
137
+ const previousInternalRole = internalManagementApiRoles.find(
138
+ ({ tenantId: internalRoleTenantId }) => internalRoleTenantId === tenantId
139
+ );
140
+ if (applicationRolesOfTheTenant.length === 0 || !previousInternalRole) {
141
+ return;
142
+ }
143
+ await pool.query<{
144
+ id: string;
145
+ applicationId: string;
146
+ roleId: string;
147
+ tenantId: string;
148
+ }>(sql`
149
+ update applications_roles set role_id = ${roleId} where tenant_id = ${tenantId} and role_id = ${
150
+ previousInternalRole.roleId
151
+ } and application_id in (${sql.join(
152
+ applicationRolesOfTheTenant.map(({ applicationId }) => applicationId),
153
+ sql`, `
154
+ )});
155
+ `);
156
+ })
157
+ );
158
+ },
159
+ down: async (pool) => {
160
+ /**
161
+ * Step 1
162
+ * Get all auto-created management api access roles.
163
+ * 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.
164
+ */
165
+ /**
166
+ * Notice that in our case:
167
+ * Each tenant has only one built-in management API resource and one attached management API access role.
168
+ * Each management API access role role has only one scope (`PredefinedScope.All`).
169
+ *
170
+ * Can go to @logto/schemas/src/{utils,seeds}/* to find more details.
171
+ *
172
+ * Based on this setup, we can use the following query to get all internal admin roles.
173
+ */
174
+ const { rows: managementApiAccessRolesCandidates } = await pool.query<{
175
+ roleId: string;
176
+ tenantId: string;
177
+ scopeId: string;
178
+ indicator: string;
179
+ }>(sql`
180
+ select roles.id as "role_id", roles.tenant_id as "tenant_id", scopes.id as "scope_id", resources.indicator as "indicator" from roles join roles_scopes on roles_scopes.role_id = roles.id and roles_scopes.tenant_id = roles.tenant_id join scopes on scopes.id = roles_scopes.scope_id and scopes.tenant_id = roles_scopes.tenant_id join resources on resources.id = scopes.resource_id and resources.tenant_id = scopes.tenant_id
181
+ where roles.name = ${managementApiAccessRoleName} and roles.description = ${managementApiAccessRoleDescription} and roles.type = ${
182
+ RoleType.MachineToMachine
183
+ } and scopes.name = ${
184
+ PredefinedScope.All
185
+ } and resources.indicator like ${getManagementApiResourceIndicator(
186
+ '%'
187
+ )} and resources.name = 'Logto Management API';
188
+ `);
189
+ // Can not directly use the result from the query unless we use subquery, separate the filter and subquery for easy understanding.
190
+ const managementApiAccessRoles = managementApiAccessRolesCandidates.filter(
191
+ ({ indicator, tenantId }) => indicator === getManagementApiResourceIndicator(tenantId)
192
+ );
193
+ /**
194
+ * Step 2
195
+ * Get all applications_roles related to the management api access role.
196
+ */
197
+ if (managementApiAccessRoles.length === 0) {
198
+ return;
199
+ }
200
+ const { rows: applicationRoles } = await pool.query<{
201
+ id: string;
202
+ applicationId: string;
203
+ roleId: string;
204
+ tenantId: string;
205
+ }>(sql`
206
+ select * from applications_roles where (role_id, tenant_id) in (values ${sql.join(
207
+ managementApiAccessRoles.map(({ roleId, tenantId }) => sql`( ${roleId}, ${tenantId} )`),
208
+ sql`, `
209
+ )});
210
+ `);
211
+ /**
212
+ * Step 3
213
+ * Find all internal admin access roles.
214
+ */
215
+ const concernedTenantIds = deduplicate(
216
+ managementApiAccessRoles.map(({ tenantId }) => tenantId)
217
+ );
218
+ const { rows: internalAdminAccessRoles } = await pool.query<{
219
+ roleId: string;
220
+ tenantId: string;
221
+ }>(sql`
222
+ select roles.id as "roleId", roles.tenant_id as "tenantId" from roles join roles_scopes on roles.tenant_id = roles_scopes.tenant_id and roles.id = roles_scopes.role_id join scopes on scopes.tenant_id = roles_scopes.tenant_id and scopes.id = roles_scopes.scope_id join resources on resources.id = scopes.resource_id and resources.tenant_id = scopes.tenant_id where roles.name = ${
223
+ InternalRole.Admin
224
+ } and ( roles.tenant_id, resources.indicator ) in (values ${sql.join(
225
+ concernedTenantIds.map(
226
+ (tenantId) => sql`( ${tenantId}, ${getManagementApiResourceIndicator(tenantId)} )`
227
+ ),
228
+ sql`, `
229
+ )});
230
+ `);
231
+ /**
232
+ * Step 4
233
+ * Assign internal admin access roles to m2m apps with management api access roles. (Found in step 2)
234
+ */
235
+ await Promise.all(
236
+ internalAdminAccessRoles.map(async ({ roleId: internalAdminAccessRoleId, tenantId }) => {
237
+ const pendingApplicationsOfTenant = applicationRoles.filter(
238
+ ({ tenantId: applicationTenantId }) => tenantId === applicationTenantId
239
+ );
240
+ const previousManagementApiAccessRole = managementApiAccessRoles.find(
241
+ ({ tenantId: managementApiAccessRoleTenantId }) =>
242
+ managementApiAccessRoleTenantId === tenantId
243
+ );
244
+ if (pendingApplicationsOfTenant.length === 0 || !previousManagementApiAccessRole) {
245
+ return;
246
+ }
247
+ await pool.query<{
248
+ id: string;
249
+ applicationId: string;
250
+ roleId: string;
251
+ tenantId: string;
252
+ }>(sql`
253
+ update applications_roles set role_id = ${internalAdminAccessRoleId} where tenant_id = ${tenantId} and role_id = ${
254
+ previousManagementApiAccessRole.roleId
255
+ } and application_id in (${sql.join(
256
+ pendingApplicationsOfTenant.map(({ applicationId }) => applicationId),
257
+ sql`, `
258
+ )});
259
+ `);
260
+ })
261
+ );
262
+ /**
263
+ * Step 5
264
+ * Remove management api access roles. (`roles_scopes` will automatically be removed if roles are removed)
265
+ */
266
+ if (managementApiAccessRoles.length === 0) {
267
+ return;
268
+ }
269
+ await Promise.all(
270
+ managementApiAccessRoles.map(async ({ roleId, tenantId }) => {
271
+ await pool.query(sql`
272
+ delete from roles where id = ${roleId} and tenant_id = ${tenantId};
273
+ `);
274
+ })
275
+ );
276
+ },
277
+ };
278
+
279
+ 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,157 @@
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 roles.id as "role_id", roles.tenant_id as "tenant_id", scopes.id as "scope_id", resources.indicator as "indicator" from roles join roles_scopes on roles_scopes.role_id = roles.id and roles_scopes.tenant_id = roles.tenant_id join scopes on scopes.id = roles_scopes.scope_id and scopes.tenant_id = roles_scopes.tenant_id join resources on resources.id = scopes.resource_id and resources.tenant_id = scopes.tenant_id
37
+ where roles.name = ${InternalRole.Admin} and roles.type = ${RoleType.MachineToMachine} and scopes.name = ${PredefinedScope.All} and resources.indicator like ${getManagementApiResourceIndicator('%')} and resources.name = 'Logto Management API'
38
+ `);
39
+ // Can not directly use the result from the query unless we use subquery, separate the filter and subquery for easy understanding.
40
+ const internalManagementApiRoles = internalManagementApiRolesCandidates.filter(({ indicator, tenantId }) => indicator === getManagementApiResourceIndicator(tenantId));
41
+ /**
42
+ * Step 2
43
+ * Get all applications_roles related to the internal admin roles.
44
+ */
45
+ const { rows: applicationRoles } = await pool.query(sql `
46
+ select * from applications_roles where (role_id, tenant_id) in (values ${sql.join(internalManagementApiRoles.map(({ roleId, tenantId }) => sql `( ${roleId}, ${tenantId} )`), sql `, `)});
47
+ `);
48
+ /**
49
+ * Step 3
50
+ * Create new roles with only management API access for tenants (m2m apps with internal admin access should share the same role),
51
+ * we can not directly assign internal admin roles to m2m applications since it's "internal" and invisible to Logto users.
52
+ */
53
+ /** A tenant can have multiple applications with internal admin role, hence need to `deduplicate()`. */
54
+ const tenantsNeedManagementApiAccessRole = deduplicate(applicationRoles.map(({ tenantId }) => tenantId));
55
+ if (tenantsNeedManagementApiAccessRole.length === 0) {
56
+ return;
57
+ }
58
+ const { rows: insertedRoles } = await pool.query(sql `
59
+ insert into roles (tenant_id, id, name, description, type) values ${sql.join(tenantsNeedManagementApiAccessRole.map((tenantId) => sql `( ${tenantId}, ${generateStandardId()}, ${managementApiAccessRoleName}, ${managementApiAccessRoleDescription}, ${RoleType.MachineToMachine} )`), sql `, `)} returning *;
60
+ `);
61
+ /**
62
+ * Step 4
63
+ * Assign internal admin access scopes to new roles.
64
+ */
65
+ await Promise.all(insertedRoles.map(async ({ tenantId, id: roleId }) => {
66
+ const internalRoleForTenant = internalManagementApiRoles.find(({ tenantId: roleTenantId }) => tenantId === roleTenantId);
67
+ if (!internalRoleForTenant) {
68
+ return;
69
+ }
70
+ await pool.query(sql `
71
+ insert into roles_scopes (tenant_id, id, role_id, scope_id) values (${tenantId}, ${generateStandardId()}, ${roleId}, ${internalRoleForTenant.scopeId});
72
+ `);
73
+ }));
74
+ /**
75
+ * Step 5
76
+ * Should remove internal admin access roles from m2m applications and assign new roles (created in step 3) to them.
77
+ * These two steps can be done by simply replace the role_id in applications_roles table.
78
+ */
79
+ await Promise.all(insertedRoles.map(async ({ tenantId, id: roleId }) => {
80
+ const applicationRolesOfTheTenant = applicationRoles.filter(({ tenantId: applicationRoleTenantId }) => tenantId === applicationRoleTenantId);
81
+ const previousInternalRole = internalManagementApiRoles.find(({ tenantId: internalRoleTenantId }) => internalRoleTenantId === tenantId);
82
+ if (applicationRolesOfTheTenant.length === 0 || !previousInternalRole) {
83
+ return;
84
+ }
85
+ await pool.query(sql `
86
+ 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 `, `)});
87
+ `);
88
+ }));
89
+ },
90
+ down: async (pool) => {
91
+ /**
92
+ * Step 1
93
+ * Get all auto-created management api access roles.
94
+ * 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.
95
+ */
96
+ /**
97
+ * Notice that in our case:
98
+ * Each tenant has only one built-in management API resource and one attached management API access role.
99
+ * Each management API access role role has only one scope (`PredefinedScope.All`).
100
+ *
101
+ * Can go to @logto/schemas/src/{utils,seeds}/* to find more details.
102
+ *
103
+ * Based on this setup, we can use the following query to get all internal admin roles.
104
+ */
105
+ const { rows: managementApiAccessRolesCandidates } = await pool.query(sql `
106
+ select roles.id as "role_id", roles.tenant_id as "tenant_id", scopes.id as "scope_id", resources.indicator as "indicator" from roles join roles_scopes on roles_scopes.role_id = roles.id and roles_scopes.tenant_id = roles.tenant_id join scopes on scopes.id = roles_scopes.scope_id and scopes.tenant_id = roles_scopes.tenant_id join resources on resources.id = scopes.resource_id and resources.tenant_id = scopes.tenant_id
107
+ where roles.name = ${managementApiAccessRoleName} and roles.description = ${managementApiAccessRoleDescription} and roles.type = ${RoleType.MachineToMachine} and scopes.name = ${PredefinedScope.All} and resources.indicator like ${getManagementApiResourceIndicator('%')} and resources.name = 'Logto Management API';
108
+ `);
109
+ // Can not directly use the result from the query unless we use subquery, separate the filter and subquery for easy understanding.
110
+ const managementApiAccessRoles = managementApiAccessRolesCandidates.filter(({ indicator, tenantId }) => indicator === getManagementApiResourceIndicator(tenantId));
111
+ /**
112
+ * Step 2
113
+ * Get all applications_roles related to the management api access role.
114
+ */
115
+ if (managementApiAccessRoles.length === 0) {
116
+ return;
117
+ }
118
+ const { rows: applicationRoles } = await pool.query(sql `
119
+ select * from applications_roles where (role_id, tenant_id) in (values ${sql.join(managementApiAccessRoles.map(({ roleId, tenantId }) => sql `( ${roleId}, ${tenantId} )`), sql `, `)});
120
+ `);
121
+ /**
122
+ * Step 3
123
+ * Find all internal admin access roles.
124
+ */
125
+ const concernedTenantIds = deduplicate(managementApiAccessRoles.map(({ tenantId }) => tenantId));
126
+ const { rows: internalAdminAccessRoles } = await pool.query(sql `
127
+ select roles.id as "roleId", roles.tenant_id as "tenantId" from roles join roles_scopes on roles.tenant_id = roles_scopes.tenant_id and roles.id = roles_scopes.role_id join scopes on scopes.tenant_id = roles_scopes.tenant_id and scopes.id = roles_scopes.scope_id join resources on resources.id = scopes.resource_id and resources.tenant_id = scopes.tenant_id where roles.name = ${InternalRole.Admin} and ( roles.tenant_id, resources.indicator ) in (values ${sql.join(concernedTenantIds.map((tenantId) => sql `( ${tenantId}, ${getManagementApiResourceIndicator(tenantId)} )`), sql `, `)});
128
+ `);
129
+ /**
130
+ * Step 4
131
+ * Assign internal admin access roles to m2m apps with management api access roles. (Found in step 2)
132
+ */
133
+ await Promise.all(internalAdminAccessRoles.map(async ({ roleId: internalAdminAccessRoleId, tenantId }) => {
134
+ const pendingApplicationsOfTenant = applicationRoles.filter(({ tenantId: applicationTenantId }) => tenantId === applicationTenantId);
135
+ const previousManagementApiAccessRole = managementApiAccessRoles.find(({ tenantId: managementApiAccessRoleTenantId }) => managementApiAccessRoleTenantId === tenantId);
136
+ if (pendingApplicationsOfTenant.length === 0 || !previousManagementApiAccessRole) {
137
+ return;
138
+ }
139
+ await pool.query(sql `
140
+ 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 `, `)});
141
+ `);
142
+ }));
143
+ /**
144
+ * Step 5
145
+ * Remove management api access roles. (`roles_scopes` will automatically be removed if roles are removed)
146
+ */
147
+ if (managementApiAccessRoles.length === 0) {
148
+ return;
149
+ }
150
+ await Promise.all(managementApiAccessRoles.map(async ({ roleId, tenantId }) => {
151
+ await pool.query(sql `
152
+ delete from roles where id = ${roleId} and tenant_id = ${tenantId};
153
+ `);
154
+ }));
155
+ },
156
+ };
157
+ 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",
@@ -11,6 +11,18 @@ export var RoleType;
11
11
  RoleType["User"] = "User";
12
12
  RoleType["MachineToMachine"] = "MachineToMachine";
13
13
  })(RoleType || (RoleType = {}));
14
+ export var SentinelActionResult;
15
+ (function (SentinelActionResult) {
16
+ SentinelActionResult["Success"] = "Success";
17
+ SentinelActionResult["Failed"] = "Failed";
18
+ })(SentinelActionResult || (SentinelActionResult = {}));
19
+ export var SentinelDecision;
20
+ (function (SentinelDecision) {
21
+ SentinelDecision["Undecided"] = "Undecided";
22
+ SentinelDecision["Allowed"] = "Allowed";
23
+ SentinelDecision["Blocked"] = "Blocked";
24
+ SentinelDecision["Challenge"] = "Challenge";
25
+ })(SentinelDecision || (SentinelDecision = {}));
14
26
  export var SignInMode;
15
27
  (function (SignInMode) {
16
28
  SignInMode["SignIn"] = "SignIn";
@@ -18,6 +18,7 @@ export * from './resource.js';
18
18
  export * from './role.js';
19
19
  export * from './roles-scope.js';
20
20
  export * from './scope.js';
21
+ export * from './sentinel-activity.js';
21
22
  export * from './service-log.js';
22
23
  export * from './sign-in-experience.js';
23
24
  export * from './system.js';
@@ -19,6 +19,7 @@ export * from './resource.js';
19
19
  export * from './role.js';
20
20
  export * from './roles-scope.js';
21
21
  export * from './scope.js';
22
+ export * from './sentinel-activity.js';
22
23
  export * from './service-log.js';
23
24
  export * from './sign-in-experience.js';
24
25
  export * from './system.js';