0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor: remove legacy resources (#5118)

* refactor: remove legacy api resources

* refactor: remove legacy resources

* refactor: add alteration script

* refactor(cli): adjust legacy seed test data

* refactor: update alteration timestamp

* refactor: update organization role name
This commit is contained in:
Gao Sun 2024-01-08 14:47:30 +08:00 committed by GitHub
parent a52a3d1663
commit 3d5786d982
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 309 additions and 157 deletions

View file

@ -41,7 +41,13 @@ import { consoleLog, getPathInModule } from '../../../utils.js';
import { appendAdminConsoleRedirectUris, seedTenantCloudServiceApplication } from './cloud.js';
import { seedOidcConfigs } from './oidc-config.js';
import { seedTenantOrganizations } from './tenant-organizations.js';
import { assignScopesToRole, createTenant, seedAdminData } from './tenant.js';
import {
assignScopesToRole,
createTenant,
seedAdminData,
seedLegacyManagementApiUserRole,
seedManagementApiProxyApplications,
} from './tenant.js';
const getExplicitOrder = (query: string) => {
const matched = /\/\*\s*init_order\s*=\s*([\d.]+)\s*\*\//.exec(query)?.[1];
@ -159,18 +165,9 @@ export const seedTables = async (
.map(({ id }) => id)
);
// Assign all cloud API scopes to role `admin:admin`
await assignScopesToRole(
connection,
adminTenantId,
adminAdminData.role.id,
...cloudAdditionalScopes.map(({ id }) => id)
);
// FIXME: @wangsijie should not create tenant Cloud Service application in the OSS DB.
await seedTenantCloudServiceApplication(connection, defaultTenantId);
await Promise.all([
seedLegacyManagementApiUserRole(connection),
seedTenantCloudServiceApplication(connection, defaultTenantId),
connection.query(
insertInto(createDefaultAdminConsoleConfig(defaultTenantId), LogtoConfigs.table)
),
@ -183,10 +180,10 @@ export const seedTables = async (
connection.query(insertInto(createAdminTenantSignInExperience(), SignInExperiences.table)),
connection.query(insertInto(createDefaultAdminConsoleApplication(), Applications.table)),
updateDatabaseTimestamp(connection, latestTimestamp),
seedTenantOrganizations(connection),
seedManagementApiProxyApplications(connection),
]);
await seedTenantOrganizations(connection);
consoleLog.succeed('Seed data');
};
@ -232,13 +229,18 @@ export const seedTest = async (connection: DatabaseTransactionConnection, forLeg
insertInto({ id: userIds[1], username: 'test2', tenantId: adminTenantId }, Users.table)
),
]);
if (forLegacy) {
const adminTenantRole = await getManagementRole(adminTenantId);
await assignRoleToUser(userIds[0], adminTenantRole.id);
}
consoleLog.succeed('Created test users');
const adminTenantRole = await getManagementRole(adminTenantId);
// The only legacy user role for Management API. Used in OSS only.
const defaultTenantRole = await getManagementRole(defaultTenantId);
await Promise.all([
assignRoleToUser(userIds[0], adminTenantRole.id),
assignRoleToUser(userIds[0], defaultTenantRole.id),
assignRoleToUser(userIds[1], defaultTenantRole.id),
]);

View file

@ -1,8 +1,6 @@
import {
defaultTenantId,
adminTenantId,
Roles,
Applications,
OrganizationRoles,
TenantRole,
getTenantScope,
@ -13,32 +11,17 @@ import {
tenantRoleScopes,
getTenantOrganizationCreateData,
Organizations,
Scopes,
Resources,
PredefinedScope,
RolesScopes,
ApplicationsRoles,
} from '@logto/schemas';
import { getMapiProxyM2mApp, getMapiProxyRole } from '@logto/schemas/lib/types/mapi-proxy.js';
import { convertToIdentifiers, generateStandardId } from '@logto/shared';
import type { DatabaseTransactionConnection } from 'slonik';
import { sql } from 'slonik';
import { insertInto } from '../../../database.js';
import { consoleLog } from '../../../utils.js';
/**
* Seed initial data in the admin tenant for tenant organizations:
* Seed initial data in the admin tenant for tenant organizations (`default` and `admin`):
*
* - Organization roles and scopes for tenant management.
* - Create tenant organizations for the initial tenants (`default` and `admin`).
*
* The following data are used for Logto Cloud, we seed them anyway for the sake of simplicity:
*
* - Machine-to-machine roles for Management API proxy.
* - Assign the corresponding Management API scopes to the machine-to-machine roles.
* - Machine-to-machine applications for Management API proxy.
* - Assign the roles to the corresponding applications.
*/
export const seedTenantOrganizations = async (connection: DatabaseTransactionConnection) => {
const tenantIds = [defaultTenantId, adminTenantId];
@ -83,73 +66,4 @@ export const seedTenantOrganizations = async (connection: DatabaseTransactionCon
)
);
consoleLog.succeed('Created tenant organizations');
/* === Cloud-specific data === */
// Init Management API proxy roles
await connection.query(
insertInto(
tenantIds.map((tenantId) => getMapiProxyRole(tenantId)),
Roles.table
)
);
consoleLog.succeed('Created Management API proxy roles');
// Prepare Management API scopes
const scopes = convertToIdentifiers(Scopes, true);
const resources = convertToIdentifiers(Resources, true);
/** Scopes with the name {@link PredefinedScope.All} in all Management API resources. */
const allScopes = await connection.any<{ id: string; indicator: string }>(sql`
select ${scopes.fields.id}, ${resources.fields.indicator}
from ${resources.table}
join ${scopes.table} on ${scopes.fields.resourceId} = ${resources.fields.id}
where ${resources.fields.indicator} like 'https://%.logto.app/api'
and ${scopes.fields.name} = ${PredefinedScope.All}
and ${resources.fields.tenantId} = ${adminTenantId}
`);
const assertScopeId = (forTenantId: string) => {
const scope = allScopes.find(
(scope) => scope.indicator === `https://${forTenantId}.logto.app/api`
);
if (!scope) {
throw new Error(`Cannot find Management API scope for tenant '${forTenantId}'.`);
}
return scope.id;
};
// Assign Management API scopes to the proxy roles
await connection.query(
insertInto(
tenantIds.map((tenantId) => ({
tenantId: adminTenantId,
id: generateStandardId(),
roleId: getMapiProxyRole(tenantId).id,
scopeId: assertScopeId(tenantId),
})),
RolesScopes.table
)
);
consoleLog.succeed('Assigned Management API scopes to the proxy roles');
// Create machine-to-machine applications for Management API proxy
await connection.query(
insertInto(
tenantIds.map((tenantId) => getMapiProxyM2mApp(tenantId)),
Applications.table
)
);
consoleLog.succeed('Created machine-to-machine applications for Management API proxy');
// Assign the proxy roles to the applications
await connection.query(
insertInto(
tenantIds.map((tenantId) => ({
tenantId: adminTenantId,
id: generateStandardId(),
applicationId: getMapiProxyM2mApp(tenantId).id,
roleId: getMapiProxyRole(tenantId).id,
})),
ApplicationsRoles.table
)
);
consoleLog.succeed('Assigned the proxy roles to the applications');
};

View file

@ -1,13 +1,29 @@
import { createTenantMetadata } from '@logto/core-kit';
import type { AdminData, UpdateAdminData, CreateScope, CreateRolesScope } from '@logto/schemas';
import {
type AdminData,
type UpdateAdminData,
type CreateScope,
type CreateRolesScope,
defaultTenantId,
adminTenantId,
Applications,
ApplicationsRoles,
getMapiProxyM2mApp,
getMapiProxyRole,
defaultManagementApiAdminName,
Roles,
PredefinedScope,
getManagementApiResourceIndicator,
} from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { assert } from '@silverhand/essentials';
import type { CommonQueryMethods } from 'slonik';
import type { CommonQueryMethods, DatabaseTransactionConnection } from 'slonik';
import { sql } from 'slonik';
import { raw } from 'slonik-sql-tag-raw';
import { insertInto } from '../../../database.js';
import { getDatabaseName } from '../../../queries/database.js';
import { consoleLog } from '../../../utils.js';
export const createTenant = async (pool: CommonQueryMethods, tenantId: string) => {
const database = await getDatabaseName(pool, true);
@ -102,3 +118,72 @@ export const assignScopesToRole = async (
)
);
};
/**
* For each initial tenant (`default` and `admin`), create a machine-to-machine application for
* Management API proxy and assign the corresponding proxy role to it.
*/
export const seedManagementApiProxyApplications = async (
connection: DatabaseTransactionConnection
) => {
const tenantIds = [defaultTenantId, adminTenantId];
// Create machine-to-machine applications for Management API proxy
await connection.query(
insertInto(
tenantIds.map((tenantId) => getMapiProxyM2mApp(tenantId)),
Applications.table
)
);
consoleLog.succeed('Created machine-to-machine applications for Management API proxy');
// Assign the proxy roles to the applications
await connection.query(
insertInto(
tenantIds.map((tenantId) => ({
tenantId: adminTenantId,
id: generateStandardId(),
applicationId: getMapiProxyM2mApp(tenantId).id,
roleId: getMapiProxyRole(tenantId).id,
})),
ApplicationsRoles.table
)
);
consoleLog.succeed('Assigned the proxy roles to the applications');
};
/**
* Seed the legacy user role for accessing default Management API, and assign the `all` scope to
* it. Used in OSS only.
*/
export const seedLegacyManagementApiUserRole = async (
connection: DatabaseTransactionConnection
) => {
const roleId = generateStandardId();
await connection.query(
insertInto(
{
tenantId: adminTenantId,
id: roleId,
name: defaultManagementApiAdminName,
description: 'Legacy user role for accessing default Management API. Used in OSS only.',
},
Roles.table
)
);
await connection.query(sql`
insert into roles_scopes (id, role_id, scope_id, tenant_id)
values (
${generateStandardId()},
${roleId},
(
select scopes.id from scopes
join resources on scopes.resource_id = resources.id
where resources.indicator = ${getManagementApiResourceIndicator(defaultTenantId)}
and scopes.name = ${PredefinedScope.All}
and scopes.tenant_id = ${adminTenantId}
),
${adminTenantId}
);
`);
};

View file

@ -94,7 +94,6 @@ function Providers() {
isCloud && [
...Object.values(TenantScope),
cloudApi.scopes.CreateTenant,
cloudApi.scopes.ManageTenant,
cloudApi.scopes.ManageTenantSelf,
]
),

View file

@ -4,7 +4,6 @@ import type { User } from '@logto/schemas';
import {
AdminTenantRole,
SignInMode,
getManagementApiAdminName,
defaultTenantId,
adminTenantId,
InteractionEvent,
@ -13,6 +12,7 @@ import {
getTenantOrganizationId,
getTenantRole,
TenantRole,
defaultManagementApiAdminName,
} from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { conditional, conditionalArray, trySafe } from '@silverhand/essentials';
@ -80,8 +80,7 @@ const getInitialUserRoles = (
) =>
conditionalArray<string>(
isInAdminTenant && AdminTenantRole.User,
isCreatingFirstAdminUser && getManagementApiAdminName(defaultTenantId),
isCreatingFirstAdminUser && isCloud && getManagementApiAdminName(adminTenantId)
isCreatingFirstAdminUser && !isCloud && defaultManagementApiAdminName // OSS uses the legacy Management API user role
);
async function handleSubmitRegister(

View file

@ -3,21 +3,16 @@
import type { LogtoConfig } from '@logto/node';
import {
cloudApiIndicator,
CloudScope,
PredefinedScope,
adminTenantId,
defaultTenantId,
getManagementApiResourceIndicator,
getManagementApiAdminName,
adminConsoleApplicationId,
InteractionEvent,
AdminTenantRole,
type Role,
type User,
RoleType,
} from '@logto/schemas';
import { conditionalArray } from '@silverhand/essentials';
import { authedAdminTenantApi as api, adminTenantApi } from '#src/api/api.js';
import type { InteractionPayload } from '#src/api/interaction.js';
@ -106,20 +101,3 @@ export const createUserWithAllRolesAndSignInToClient = async () => {
return { id, client };
};
export const createUserAndSignInToCloudClient = async (
userRoleType: AdminTenantRole.User | AdminTenantRole.Admin
) => {
const [{ id }, { username, password }] = await createUserWithRoles(
conditionalArray<string>(
AdminTenantRole.User,
userRoleType === AdminTenantRole.Admin && getManagementApiAdminName(adminTenantId)
)
);
const client = await initClientAndSignIn(username, password, {
resources: [cloudApiIndicator],
scopes: Object.values(CloudScope),
});
return { id, client };
};

View file

@ -0,0 +1,169 @@
import { generateStandardId } from '@logto/shared/universal';
import { sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const alteration: AlterationScript = {
up: async (pool) => {
// Unassign cloud scopes accidentally assigned to the admin Management API proxy
await pool.query(sql`
delete from roles_scopes
using scopes
where roles_scopes.tenant_id = 'admin'
and roles_scopes.role_id = 'm-admin'
and roles_scopes.scope_id = scopes.id
and scopes.name in ('send:sms', 'send:email', 'create:affiliate', 'manage:affiliate');
`);
// Delete all legacy roles in the admin tenant
await pool.query(sql`
delete from roles
where tenant_id = 'admin'
and name like '%:admin'
and name not like 'machine:mapi:%'
and name != 'default:admin';
`);
// Update default role description
await pool.query(sql`
update roles
set description = 'Legacy user role for accessing default Management API. Used in OSS only.'
where tenant_id = 'admin'
and name = 'default:admin';
`);
// Delete `manage:tenant` scope from the Cloud API resource
await pool.query(sql`
delete from scopes
using resources
where scopes.tenant_id = 'admin'
and scopes.name = 'manage:tenant'
and scopes.resource_id = resources.id
and resources.indicator = 'https://cloud.logto.io/api';
`);
},
down: async (pool) => {
console.log('Add `manage:tenant` scope to the Cloud API resource');
// Add `manage:tenant` scope to the Cloud API resource
await pool.query(sql`
insert into scopes (tenant_id, id, name, description, resource_id)
values ('admin', 'manage:tenant', 'manage:tenant', 'Allow managing existing tenants, including create without limitation, update, and delete.', (
select id from resources where tenant_id = 'admin' and indicator = 'https://cloud.logto.io/api'
));
`);
console.log('Update default role description');
// Update default role description
await pool.query(sql`
update roles
set description = 'Admin tenant admin role for Logto tenant default.'
where tenant_id = 'admin'
and name = 'default:admin';
`);
console.log('Add legacy roles in the admin tenant');
// Add legacy roles in the admin tenant
const existingTenantIds = await pool.any<{ id: string }>(sql`
select id from tenants where id != 'default';
`);
await pool.query(sql`
insert into roles (tenant_id, id, name, description)
values ${sql.join(
existingTenantIds.map((tenant) => {
return sql`
(
'admin',
${`${tenant.id}:admin`},
${`${tenant.id}:admin`},
${`Admin tenant admin role for Logto tenant ${tenant.id}.`}
)
`;
}),
sql`, `
)};
`);
console.log('Restore assigned Management API scopes to the legacy roles');
// Restore assigned Management API scopes to the legacy roles
await pool.query(sql`
insert into roles_scopes (tenant_id, id, role_id, scope_id)
values ${sql.join(
existingTenantIds.map((tenant) => {
return sql`
(
'admin',
${`${tenant.id}:admin`},
${`${tenant.id}:admin`},
(
select scopes.id from scopes
join resources on scopes.resource_id = resources.id
and resources.indicator = ${`https://${tenant.id}.logto.app/api`}
where scopes.tenant_id = 'admin'
and scopes.name = 'all'
)
)
`;
}),
sql`, `
)};
`);
console.log('Assign to legacy roles to users according to the tenant organization roles');
// Assign to legacy roles to users according to the tenant organization roles
const adminUsersOrganizations = await pool.any<{ userId: string; organizationId: string }>(sql`
select user_id as "userId", organization_id as "organizationId"
from organization_role_user_relations
where tenant_id = 'admin'
and organization_role_id = 'admin'
and organization_id != 't-default'
and organization_id like 't-%';
`);
await pool.query(sql`
insert into users_roles (tenant_id, id, user_id, role_id)
values ${sql.join(
adminUsersOrganizations.map((relation) => {
return sql`
(
'admin',
${`${relation.userId}:${relation.organizationId.slice(2)}:admin`},
${relation.userId},
${`${relation.organizationId.slice(2)}:admin`}
)
`;
}),
sql`, `
)};
`);
console.log(
'Assign back cloud scopes to the admin Management API proxy and the legacy admin user'
);
// Assign back cloud scopes to the admin Management API proxy and the legacy admin user
await pool.query(sql`
insert into roles_scopes (tenant_id, id, role_id, scope_id)
values ${sql.join(
['send:sms', 'send:email', 'create:affiliate', 'manage:affiliate', 'manage:tenant'].map(
(scope) => {
return sql`
(
'admin',
${generateStandardId()},
'm-admin',
(
select id from scopes
where tenant_id = 'admin'
and name = ${scope}
)
),
(
'admin',
${generateStandardId()},
'admin:admin',
(
select id from scopes
where tenant_id = 'admin'
and name = ${scope}
)
)
`;
}
),
sql`, `
)};
`);
},
};
export default alteration;

View file

@ -13,8 +13,6 @@ export const cloudApiIndicator = 'https://cloud.logto.io/api';
export enum CloudScope {
/** The user can create a user tenant. */
CreateTenant = 'create:tenant',
/** The user can perform arbitrary operations on any tenant. */
ManageTenant = 'manage:tenant',
/** The user can update or delete its own tenants. */
ManageTenantSelf = 'manage:tenant:self',
SendSms = 'send:sms',
@ -55,10 +53,6 @@ export const createCloudApi = (): Readonly<[UpdateAdminData, ...CreateScope[]]>
name: AdminTenantRole.User,
},
},
buildScope(
CloudScope.ManageTenant,
'Allow managing existing tenants, including create without limitation, update, and delete.'
),
buildScope(
CloudScope.SendEmail,
'Allow sending emails. This scope is only available to M2M application.'

View file

@ -6,10 +6,19 @@ import {
type CreateRole,
type CreateScope,
} from '../db-entries/index.js';
import { PredefinedScope, InternalRole, AdminTenantRole } from '../types/index.js';
import {
PredefinedScope,
InternalRole,
AdminTenantRole,
getMapiProxyRole,
} from '../types/index.js';
import { adminTenantId, defaultTenantId } from './tenant.js';
/**
* The Management API data for a tenant. Usually used for creating a new tenant in the admin
* tenant.
*/
export type AdminData = {
resource: CreateResource;
scopes: CreateScope[];
@ -51,6 +60,10 @@ export const defaultManagementApi = Object.freeze({
resourceId: defaultResourceId,
},
],
/**
* An internal user role for Management API of the `default` tenant.
* @deprecated This role will be removed soon.
*/
role: {
tenantId: defaultTenantId,
/** @deprecated You should not rely on this constant. Change to something else. */
@ -73,11 +86,14 @@ export function getManagementApiResourceIndicator(tenantId: string, path = 'api'
return `https://${tenantId}.logto.app/${path}`;
}
export const getManagementApiAdminName = <TenantId extends string>(tenantId: TenantId) =>
`${tenantId}:${AdminTenantRole.Admin}` as const;
/**
* The fixed Management API user role for `default` tenant in the admin tenant. It is used for
* OSS only.
*/
export const defaultManagementApiAdminName = `${defaultTenantId}:admin` as const;
/** Create a set of admin data for Management API of the given tenant ID. */
export const createAdminData = (tenantId: string): AdminData => {
export const createAdminData = (tenantId: string) => {
const resourceId = generateStandardId();
return Object.freeze({
@ -96,6 +112,7 @@ export const createAdminData = (tenantId: string): AdminData => {
resourceId,
},
],
/** @deprecated This role will be removed soon. */
role: {
tenantId,
id: generateStandardId(),
@ -103,11 +120,11 @@ export const createAdminData = (tenantId: string): AdminData => {
description: `Internal admin role for Logto tenant ${defaultTenantId}.`,
type: RoleType.MachineToMachine,
},
});
} satisfies AdminData);
};
/** Create a set of admin data for Management API of the given tenant ID for `admin` tenant. */
export const createAdminDataInAdminTenant = (tenantId: string): AdminData => {
/** Create a set of admin data for Management API of the given tenant ID for the admin tenant. */
export const createAdminDataInAdminTenant = (tenantId: string) => {
const resourceId = generateStandardId();
return Object.freeze({
@ -126,17 +143,12 @@ export const createAdminDataInAdminTenant = (tenantId: string): AdminData => {
resourceId,
},
],
role: {
tenantId: adminTenantId,
id: generateStandardId(),
name: getManagementApiAdminName(tenantId),
description: `Admin tenant admin role for Logto tenant ${tenantId}.`,
type: RoleType.User,
},
});
/** The machine-to-machine role for the Management API proxy of the given tenant ID. */
role: getMapiProxyRole(tenantId),
} satisfies AdminData);
};
export const createMeApiInAdminTenant = (): AdminData => {
export const createMeApiInAdminTenant = () => {
const resourceId = generateStandardId();
return Object.freeze({
@ -162,5 +174,5 @@ export const createMeApiInAdminTenant = (): AdminData => {
description: 'Default role for admin tenant.',
type: RoleType.User,
},
});
} satisfies AdminData);
};

View file

@ -24,3 +24,4 @@ export * from './organization.js';
export * from './sso-connector.js';
export * from './tenant.js';
export * from './tenant-organization.js';
export * from './mapi-proxy.js';

View file

@ -13,7 +13,7 @@
* This module provides utilities to manage mapi proxy.
*/
import { generateStandardSecret } from '@logto/shared';
import { generateStandardSecret } from '@logto/shared/universal';
import {
RoleType,
@ -21,7 +21,7 @@ import {
type CreateApplication,
ApplicationType,
} from '../db-entries/index.js';
import { adminTenantId } from '../index.js';
import { adminTenantId } from '../seeds/tenant.js';
/**
* Given a tenant ID, return the role data for the mapi proxy.

View file

@ -54,7 +54,6 @@ export enum InternalRole {
}
export enum AdminTenantRole {
Admin = 'admin',
/** Common user role in admin tenant. */
User = 'user',
/** The role for machine to machine applications that represent a user tenant and send requests to Logto Cloud. */