diff --git a/README.OGCIO.md b/README.OGCIO.md index 9a1f976a5e1..26d53169c73 100644 --- a/README.OGCIO.md +++ b/README.OGCIO.md @@ -113,3 +113,17 @@ This command can take a parameter to specify the input data file, called `seeder Usage: `npm run cli db ogcio -- --seeder-filepath="DATA_FILE_PATH"` To seed the default data for local dev environments, run `npm run cli db ogcio -- --seeder-filepath="./packages/cli/src/commands/database/ogcio/ogcio-seeder-local.json"`. + +### Limitations + +In most cases, we have predefined IDs in our seeder to ensure the same database structure, even if the database was cleared and re-seeded. Logto IDs are simple text fields (no UUID or other validations are applied) with a maximum length of 21 characters. Do not use IDs longer than 21 characters; otherwise, the seeder will fail with a database error! + +Be careful when defining a new ID because data duplication is avoided based on this field. If you later want to change the ID of any of your entries, the seeder won't be able to detect the existence of the affected entry, and it will try to create a new one. Creating a duplicate entry with a different ID can cause a database error if some other fields have a unique constraint. If this is not the case, a duplicate entry will be created, which is also a mistake, and we want to avoid any of these situations. Once you have defined an ID, do not change it if unnecessary. + +Using resources other than those declared in the seeder's data file is also impossible because referencing any resource outside of the seeder's scope is not supported. The seeder is supposed to create all the required resources and use them to seed the custom configuration into the database. + +### Edge cases + +Some changes might affect other entries from the database, like the user entities. In this case, a custom migration script is required to resolve the changes necessary to the affected entries. Before any change in the seeder data, analyse the situation to determine if it is safe to perform. The seeder is not intended to resolve conflicts or update other data than the configuration it seeds. + +Deletion of existing seeded data via the seeder is not yet possible. Only the permissions (scopes) will be removed and recreated every time the seeder is executed because that is safe and does not cause conflicts with other entries. A custom script or manual action is required for any other data that must be eliminated. diff --git a/packages/cli/src/commands/database/ogcio/applications.ts b/packages/cli/src/commands/database/ogcio/applications.ts index 1027c92ba97..42c4a47b3bf 100644 --- a/packages/cli/src/commands/database/ogcio/applications.ts +++ b/packages/cli/src/commands/database/ogcio/applications.ts @@ -5,7 +5,7 @@ import { Applications } from '@logto/schemas'; import { sql, type DatabaseTransactionConnection } from '@silverhand/slonik'; import { type ApplicationSeeder } from './ogcio-seeder.js'; -import { createItem } from './queries.js'; +import { createOrUpdateItem } from './queries.js'; type SeedingApplication = { id: string; @@ -24,12 +24,12 @@ const createApplication = async ( tenantId: string, appToSeed: SeedingApplication ) => - createItem({ + createOrUpdateItem({ transaction, tenantId, toInsert: appToSeed, toLogFieldName: 'name', - whereClauses: [sql`name = ${appToSeed.name}`], + whereClauses: [sql`tenant_id = ${tenantId}`, sql`id = ${appToSeed.id}`], tableName: Applications.table, }); diff --git a/packages/cli/src/commands/database/ogcio/connectors.ts b/packages/cli/src/commands/database/ogcio/connectors.ts index c891019042c..1dd3ab0d92a 100644 --- a/packages/cli/src/commands/database/ogcio/connectors.ts +++ b/packages/cli/src/commands/database/ogcio/connectors.ts @@ -5,7 +5,7 @@ import { Connectors } from '@logto/schemas'; import { sql, type DatabaseTransactionConnection } from '@silverhand/slonik'; import { type ConnectorSeeder } from './ogcio-seeder.js'; -import { createItemWithoutId } from './queries.js'; +import { createOrUpdateItemWithoutId } from './queries.js'; type SeedingConnector = { tenant_id: string; @@ -21,12 +21,12 @@ const createConnector = async ( tenantId: string, connectorToSeed: SeedingConnector ) => - createItemWithoutId({ + createOrUpdateItemWithoutId({ transaction, tenantId, toInsert: connectorToSeed, toLogFieldName: 'id', - whereClauses: [sql`id = ${connectorToSeed.id}`], + whereClauses: [sql`tenant_id = ${tenantId}`, sql`id = ${connectorToSeed.id}`], tableName: Connectors.table, columnToGet: 'id', }); diff --git a/packages/cli/src/commands/database/ogcio/ogcio-seeder-local.json b/packages/cli/src/commands/database/ogcio/ogcio-seeder-local.json index d4f226c38e0..3b52e50b2af 100644 --- a/packages/cli/src/commands/database/ogcio/ogcio-seeder-local.json +++ b/packages/cli/src/commands/database/ogcio/ogcio-seeder-local.json @@ -21,6 +21,7 @@ }, "organization_roles": [ { + "id": "bb-public-servant", "name": "Public Servant", "description": "Building Blocks Public servant", "specific_permissions": [ @@ -31,6 +32,7 @@ ] }, { + "id": "msg-public-servant", "name": "Messaging Public Servant", "description": "Messaging Public servant", "specific_permissions": [ @@ -94,6 +96,7 @@ ], "resource_roles": [ { + "id": "bb-citizen", "name": "Citizen", "description": "A citizen using Life Events and the Building Blocks ecosystem", "permissions": [ @@ -173,7 +176,7 @@ ], "webhooks": [ { - "id": "login_webhook", + "id": "login-webhook", "name": "User log in", "events": [ "PostRegister", diff --git a/packages/cli/src/commands/database/ogcio/ogcio-seeder.json b/packages/cli/src/commands/database/ogcio/ogcio-seeder.json index 864b259cd69..924d446b53d 100644 --- a/packages/cli/src/commands/database/ogcio/ogcio-seeder.json +++ b/packages/cli/src/commands/database/ogcio/ogcio-seeder.json @@ -21,6 +21,7 @@ }, "organization_roles": [ { + "id": "bb-public-servant", "name": "Public Servant", "description": "Building Blocks Public servant", "specific_permissions": [ @@ -31,6 +32,7 @@ ] }, { + "id": "msg-public-servant", "name": "Messaging Public Servant", "description": "Messaging Public servant", "specific_permissions": [ @@ -49,7 +51,8 @@ "redirect_uri": "", "logout_redirect_uri": "", "secret": "", - "id": "r5f56tpkytpqyyshiutd2" + "id": "r5f56tpkytpqyyshiutd2", + "is_third_party": false }, { "name": "Messaging Building Block", @@ -58,7 +61,8 @@ "redirect_uri": "", "logout_redirect_uri": "", "secret": "", - "id": "1lvmteh2ao3xrswyq7j3e" + "id": "1lvmteh2ao3xrswyq7j3e", + "is_third_party": false } ], "resources": [ @@ -92,6 +96,7 @@ ], "resource_roles": [ { + "id": "bb-citizen", "name": "Citizen", "description": "A citizen using Life Events and the Building Blocks ecosystem", "permissions": [ @@ -171,7 +176,7 @@ ], "webhooks": [ { - "id": "login_webhook", + "id": "login-webhook", "name": "User log in", "events": [ "PostRegister", diff --git a/packages/cli/src/commands/database/ogcio/ogcio-seeder.ts b/packages/cli/src/commands/database/ogcio/ogcio-seeder.ts index fb976d82c07..15d42d58c26 100644 --- a/packages/cli/src/commands/database/ogcio/ogcio-seeder.ts +++ b/packages/cli/src/commands/database/ogcio/ogcio-seeder.ts @@ -19,7 +19,7 @@ export type OgcioSeeder = { export type OrganizationSeeder = { name: string; description: string; - id?: string; + id: string; }; export type OrganizationPermissionSeeder = { @@ -27,18 +27,19 @@ export type OrganizationPermissionSeeder = { }; export type OrganizationRoleSeeder = { + id: string; name: string; specific_permissions: string[]; description: string; }; export type ApplicationSeeder = { + id: string; name: string; description: string; type: string; redirect_uri: string | string[]; logout_redirect_uri: string | string[]; - id: string; secret: string; is_third_party?: boolean; }; @@ -105,6 +106,7 @@ export type ResourcePermissionSeeder = { }; export type ResourceRoleSeeder = { + id: string; name: string; description: string; permissions: ScopePerResourceRoleSeeder[]; diff --git a/packages/cli/src/commands/database/ogcio/organizations-rbac.ts b/packages/cli/src/commands/database/ogcio/organizations-rbac.ts index a786a87e24e..270caf3a18e 100644 --- a/packages/cli/src/commands/database/ogcio/organizations-rbac.ts +++ b/packages/cli/src/commands/database/ogcio/organizations-rbac.ts @@ -1,11 +1,18 @@ /* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -/* eslint-disable @silverhand/fp/no-mutating-methods */ -import { OrganizationRoles, OrganizationRoleScopeRelations, OrganizationScopes } from '@logto/schemas'; +/* eslint-disable @silverhand/fp/no-mutation */ +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ + +import { + OrganizationRoles, + OrganizationRoleScopeRelations, + OrganizationScopes, +} from '@logto/schemas'; import { sql, type DatabaseTransactionConnection } from '@silverhand/slonik'; + import { type OrganizationPermissionSeeder, type OrganizationRoleSeeder } from './ogcio-seeder.js'; -import { createItem, createItemWithoutId } from './queries.js'; +import { createOrUpdateItem, createOrUpdateItemWithoutId, deleteQuery } from './queries.js'; type SeedingScope = { name: string; @@ -16,7 +23,7 @@ type ScopesByName = Record; type SeedingRole = { name: string; - id?: string; + id: string; description: string; scopes: string[]; }; @@ -28,24 +35,22 @@ const createScope = async (params: { tenantId: string; scopeToSeed: SeedingScope; }) => - createItem({ + createOrUpdateItem({ transaction: params.transaction, tenantId: params.tenantId, toInsert: params.scopeToSeed, toLogFieldName: 'name', tableName: OrganizationScopes.table, - whereClauses: [sql`name = ${params.scopeToSeed.name}`], + whereClauses: [sql`tenant_id = ${params.tenantId}`, sql`name = ${params.scopeToSeed.name}`], }); -const buildScopes = ( - scopes: string[] -): ScopesByName => { - return scopes.reduce((acc, scopeName) => { - acc[scopeName] = { +const buildScopes = (scopes: string[]): ScopesByName => { + return scopes.reduce((accumulator, scopeName) => { + accumulator[scopeName] = { name: scopeName, - description: scopeName + description: scopeName, }; - return acc; + return accumulator; }, {}); }; @@ -56,11 +61,11 @@ export const createScopes = async (params: { }) => { const scopesToCreate = buildScopes(params.scopesToSeed.specific_permissions); - const queries = Object.values(scopesToCreate).map((scope) => + const queries = Object.values(scopesToCreate).map(async (scope) => createScope({ transaction: params.transaction, tenantId: params.tenantId, - scopeToSeed: scope + scopeToSeed: scope, }) ); @@ -75,62 +80,63 @@ const createRole = async (params: { roleToSeed: { name: string; description: string; - id?: string; - } + id: string; + }; }) => { - const created = await createItem({ + await createOrUpdateItem({ transaction: params.transaction, tenantId: params.tenantId, toLogFieldName: 'name', - whereClauses: [sql`name = ${params.roleToSeed.name}`], - toInsert: { name: params.roleToSeed.name, description: params.roleToSeed.description }, + whereClauses: [sql`tenant_id = ${params.tenantId}`, sql`id = ${params.roleToSeed.id}`], + toInsert: { + id: params.roleToSeed.id, + name: params.roleToSeed.name, + description: params.roleToSeed.description, + }, tableName: OrganizationRoles.table, }); - params.roleToSeed.id = created.id; - - return { - ...params.roleToSeed, - id: created.id, - }; -} + return params.roleToSeed; +}; const createRoles = async (params: { transaction: DatabaseTransactionConnection; tenantId: string; - scopes: ScopesByName, - rolesToSeed: OrganizationRoleSeeder[] + scopes: ScopesByName; + rolesToSeed: OrganizationRoleSeeder[]; }) => { - const rolesToCreate = params.rolesToSeed.map((role) =>({ + const rolesToCreate = params.rolesToSeed.map((role) => ({ + id: role.id, name: role.name, description: role.description, - scopes: role.specific_permissions + scopes: role.specific_permissions, })); - const queries = rolesToCreate.map((role) => + const queries = rolesToCreate.map(async (role) => createRole({ transaction: params.transaction, tenantId: params.tenantId, - roleToSeed: role + roleToSeed: role, }) ); await Promise.all(queries); return rolesToCreate; -} +}; const createRoleScopeRelation = async ( transaction: DatabaseTransactionConnection, tenantId: string, relation: SeedingRelation ) => - createItemWithoutId({ + createOrUpdateItemWithoutId({ transaction, tableName: OrganizationRoleScopeRelations.table, tenantId, toLogFieldName: 'organization_role_id', whereClauses: [ + sql`tenant_id = ${tenantId}`, sql`organization_role_id = ${relation.organization_role_id}`, sql`organization_scope_id = ${relation.organization_scope_id}`, ], @@ -145,15 +151,34 @@ const createRelations = async ( roles: SeedingRole[] ) => { const queries = roles.flatMap((role) => - role.scopes.map((scope) => createRoleScopeRelation(transaction, tenantId, { - organization_role_id: role.id!, - organization_scope_id: scopes[scope]?.id! - })) + role.scopes.map(async (scope) => + createRoleScopeRelation(transaction, tenantId, { + organization_role_id: role.id, + organization_scope_id: scopes[scope]?.id!, + }) + ) ); return Promise.all(queries); }; +export const cleanScopes = async (transaction: DatabaseTransactionConnection, tenantId: string) => { + await cleanScopeRelations(transaction, tenantId); + const deleteQueryString = deleteQuery([sql`tenant_id = ${tenantId}`], OrganizationScopes.table); + return transaction.query(deleteQueryString); +}; + +export const cleanScopeRelations = async ( + transaction: DatabaseTransactionConnection, + tenantId: string +) => { + const deleteQueryString = deleteQuery( + [sql`tenant_id = ${tenantId}`], + OrganizationRoleScopeRelations.table + ); + return transaction.query(deleteQueryString); +}; + export const seedOrganizationRbacData = async (params: { transaction: DatabaseTransactionConnection; tenantId: string; @@ -163,6 +188,8 @@ export const seedOrganizationRbacData = async (params: { }; }) => { if (params.toSeed.organization_permissions && params.toSeed.organization_roles?.length) { + await cleanScopes(params.transaction, params.tenantId); + const createdScopes = await createScopes({ transaction: params.transaction, tenantId: params.tenantId, @@ -173,14 +200,9 @@ export const seedOrganizationRbacData = async (params: { transaction: params.transaction, tenantId: params.tenantId, scopes: createdScopes, - rolesToSeed: params.toSeed.organization_roles + rolesToSeed: params.toSeed.organization_roles, }); - await createRelations( - params.transaction, - params.tenantId, - createdScopes, - createdRoles - ); + await createRelations(params.transaction, params.tenantId, createdScopes, createdRoles); } }; diff --git a/packages/cli/src/commands/database/ogcio/organizations.ts b/packages/cli/src/commands/database/ogcio/organizations.ts index 372a8e21c55..5275c41433a 100644 --- a/packages/cli/src/commands/database/ogcio/organizations.ts +++ b/packages/cli/src/commands/database/ogcio/organizations.ts @@ -17,7 +17,7 @@ import { insertInto } from '../../../database.js'; import { consoleLog } from '../../../utils.js'; import { type OrganizationSeeder } from './ogcio-seeder.js'; -import { createItem, getInsertedColumnValue, updateQuery } from './queries.js'; +import { createOrUpdateItem, getInsertedColumnValue, updateQuery } from './queries.js'; const createAdminConsoleConfig = ( forTenantId: string @@ -43,7 +43,7 @@ const updateTenantConfigs = async ( const currentValue = await getInsertedColumnValue({ transaction, tenantId, - whereClauses: [sql`key = ${LogtoTenantConfigKey.AdminConsole}`], + whereClauses: [sql`key = ${LogtoTenantConfigKey.AdminConsole}`, sql`tenant_id = ${tenantId}`], tableName: LogtoConfigs.table, columnToGet: 'value', }); @@ -73,15 +73,15 @@ const createOrganization = async (params: { tenantId: string; organizationSeeder: OrganizationSeeder; }) => { - const organization = createItem({ + const organization = createOrUpdateItem({ transaction: params.transaction, tenantId: params.tenantId, toInsert: { name: params.organizationSeeder.name, description: params.organizationSeeder.description, - id: params.organizationSeeder.id ?? undefined, + id: params.organizationSeeder.id, }, - whereClauses: [sql`name = ${params.organizationSeeder.name}`], + whereClauses: [sql`tenant_id = ${params.tenantId}`, sql`id = ${params.organizationSeeder.id}`], toLogFieldName: 'name', tableName: Organizations.table, }); diff --git a/packages/cli/src/commands/database/ogcio/queries.ts b/packages/cli/src/commands/database/ogcio/queries.ts index f065694843b..d2498d004fc 100644 --- a/packages/cli/src/commands/database/ogcio/queries.ts +++ b/packages/cli/src/commands/database/ogcio/queries.ts @@ -22,7 +22,7 @@ export const getColumnValueByQueryResult = >( columnToGet: string ): string | undefined => { const camelColumn = snakeToCamel(columnToGet); - if (result.rows[0] === undefined || result.rows[0][camelColumn] === undefined) { + if (result.rows[0]?.[camelColumn] === undefined) { return undefined; } @@ -63,8 +63,23 @@ export const getInsertedId = async ( ): Promise => getInsertedColumnValue({ transaction, tenantId, whereClauses, tableName, columnToGet: 'id' }); -export const createItem = async < - T extends { id?: string } & Record, +const updateItem = async (params: { + transaction: DatabaseTransactionConnection; + toUpdate: Record; + whereClauses: ValueExpression[]; + tableName: string; +}) => { + const toUpdate = Object.entries(params.toUpdate).map( + ([key, value]) => sql`${sql.identifier([key])} = ${value ?? ''}` + ); + + const updateQueryString = updateQuery(toUpdate, params.whereClauses, params.tableName); + + return params.transaction.query(updateQueryString); +}; + +export const createOrUpdateItem = async < + T extends { id?: string } & Record, >(params: { transaction: DatabaseTransactionConnection; tenantId?: string; @@ -84,7 +99,16 @@ export const createItem = async < params.tableName ); if (scopeIdBefore !== undefined) { - consoleLog.info(`${prefixConsoleEntry}. Already exists.`); + consoleLog.info(`${prefixConsoleEntry}. Already exists. Updating entry.`); + + await updateItem({ + transaction: params.transaction, + toUpdate: params.toInsert, + whereClauses: params.whereClauses, + tableName: params.tableName, + }); + + consoleLog.info(`${prefixConsoleEntry}. Entry updated successfully.`); params.toInsert.id = scopeIdBefore; return { ...params.toInsert, id: scopeIdBefore }; } @@ -94,9 +118,7 @@ export const createItem = async < tenant_id: params.tenantId, }; - if (!toInsertData.id) { - toInsertData.id = generateStandardId(); - } + toInsertData.id ||= generateStandardId(); await params.transaction.query(insertInto(toInsertData, params.tableName)); params.toInsert.id = await getInsertedId( @@ -117,8 +139,8 @@ export const createItem = async < } }; -export const createItemWithoutId = async < - T extends Record, +export const createOrUpdateItemWithoutId = async < + T extends Record, >(params: { transaction: DatabaseTransactionConnection; tenantId: string | undefined; @@ -134,7 +156,16 @@ export const createItemWithoutId = async < consoleLog.info(prefixConsoleEntry); const scopeIdBefore = await getInsertedColumnValue(params); if (scopeIdBefore !== undefined) { - consoleLog.info(`${prefixConsoleEntry}. Already exists.`); + consoleLog.info(`${prefixConsoleEntry}. Already exists. Updating entry.`); + + await updateItem({ + transaction: params.transaction, + toUpdate: params.toInsert, + whereClauses: params.whereClauses, + tableName: params.tableName, + }); + + consoleLog.info(`${prefixConsoleEntry}. Entry updated successfully.`); return { ...params.toInsert, [params.columnToGet]: scopeIdBefore }; } @@ -163,3 +194,10 @@ export const updateQuery = ( where ${sql.join(whereClauses, sql` AND `)} `; }; + +export const deleteQuery = (whereClauses: ValueExpression[], table: string) => { + return sql` + delete from ${sql.identifier([table])} + where ${sql.join(whereClauses, sql` AND `)} + `; +}; diff --git a/packages/cli/src/commands/database/ogcio/resources-rbac.ts b/packages/cli/src/commands/database/ogcio/resources-rbac.ts index fcda585e7ee..5e3f704567d 100644 --- a/packages/cli/src/commands/database/ogcio/resources-rbac.ts +++ b/packages/cli/src/commands/database/ogcio/resources-rbac.ts @@ -1,12 +1,20 @@ /* eslint-disable eslint-comments/disable-enable-pair */ -/* eslint-disable @silverhand/fp/no-let */ + /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @silverhand/fp/no-mutating-methods */ /* eslint-disable @silverhand/fp/no-mutation */ + +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ + import { Roles, RolesScopes, Scopes } from '@logto/schemas'; import { sql, type DatabaseTransactionConnection } from '@silverhand/slonik'; -import { type ResourceRoleSeeder, type ResourcePermissionSeeder, ScopePerResourceRoleSeeder } from './ogcio-seeder.js'; -import { createItem } from './queries.js'; + +import { + type ResourceRoleSeeder, + type ResourcePermissionSeeder, + type ScopePerResourceRoleSeeder, +} from './ogcio-seeder.js'; +import { createOrUpdateItem, deleteQuery } from './queries.js'; import { type SeedingResource } from './resources.js'; type SeedingScope = { @@ -18,11 +26,11 @@ type SeedingScope = { type ScopesByName = Record; type ScopesByResourceId = Record; type SeededRole = { - id?: string; + id: string; name: string; description: string; - scopes: ScopePerResourceRoleSeeder[] -} + scopes: ScopePerResourceRoleSeeder[]; +}; type SeedingRelation = { role_id: string; scope_id: string; id?: string }; const createScope = async (params: { @@ -30,29 +38,27 @@ const createScope = async (params: { tenantId: string; scopeToSeed: SeedingScope; }) => - createItem({ + createOrUpdateItem({ transaction: params.transaction, tenantId: params.tenantId, toInsert: params.scopeToSeed, toLogFieldName: 'name', tableName: Scopes.table, whereClauses: [ + sql`tenant_id = ${params.tenantId}`, sql`name = ${params.scopeToSeed.name}`, - sql`resource_id = ${params.scopeToSeed.resource_id}` - ] + sql`resource_id = ${params.scopeToSeed.resource_id}`, + ], }); -const buildScopes = ( - resourceId: string, - scopes: string[] -): ScopesByName => { - return scopes.reduce((acc, scopeName) => { - acc[scopeName] = { +const buildScopes = (resourceId: string, scopes: string[]): ScopesByName => { + return scopes.reduce((accumulator, scopeName) => { + accumulator[scopeName] = { name: scopeName, description: scopeName, - resource_id: resourceId + resource_id: resourceId, }; - return acc; + return accumulator; }, {}); }; @@ -67,16 +73,16 @@ export const createScopes = async (params: { const scopesToCreate: ScopesByResourceId = {}; for (const singleSeeder of params.scopesToSeed) { - scopesToCreate[singleSeeder.resource_id] = buildScopes(singleSeeder.resource_id, singleSeeder.specific_permissions); + scopesToCreate[singleSeeder.resource_id] = buildScopes( + singleSeeder.resource_id, + singleSeeder.specific_permissions + ); } - const queries: Array< - Promise & { id: string }> - > = []; - + const queries: Array & { id: string }>> = []; - Object.values(scopesToCreate).forEach((scopes) => { - Object.values(scopes).forEach((scope) => { + for (const scopes of Object.values(scopesToCreate)) { + for (const scope of Object.values(scopes)) { queries.push( createScope({ scopeToSeed: scope, @@ -84,8 +90,8 @@ export const createScopes = async (params: { tenantId: params.tenantId, }) ); - }) - }); + } + } await Promise.all(queries); @@ -98,62 +104,66 @@ const createRole = async (params: { roleToSeed: { name: string; description: string; - id?: string; - } + id: string; + }; }) => { - const created = await createItem({ + await createOrUpdateItem({ transaction: params.transaction, tenantId: params.tenantId, toLogFieldName: 'name', - whereClauses: [sql`name = ${params.roleToSeed.name}`], - toInsert: { name: params.roleToSeed.name, description: params.roleToSeed.description }, + whereClauses: [sql`tenant_id = ${params.tenantId}`, sql`id = ${params.roleToSeed.id}`], + toInsert: { + id: params.roleToSeed.id, + name: params.roleToSeed.name, + description: params.roleToSeed.description, + }, tableName: Roles.table, }); - params.roleToSeed.id = created.id; - - return { - ...params.roleToSeed, - id: created.id, - }; -} + return params.roleToSeed; +}; const createRoles = async (params: { transaction: DatabaseTransactionConnection; tenantId: string; - scopes: ScopesByResourceId, - rolesToSeed: ResourceRoleSeeder[] + scopes: ScopesByResourceId; + rolesToSeed: ResourceRoleSeeder[]; }) => { - const rolesToCreate = params.rolesToSeed.map((role) =>({ + const rolesToCreate = params.rolesToSeed.map((role) => ({ + id: role.id, name: role.name, description: role.description, - scopes: role.permissions + scopes: role.permissions, })); - const queries = rolesToCreate.map((role) => + const queries = rolesToCreate.map(async (role) => createRole({ transaction: params.transaction, tenantId: params.tenantId, - roleToSeed: role + roleToSeed: role, }) ); await Promise.all(queries); return rolesToCreate; -} +}; const createRoleScopeRelation = async ( transaction: DatabaseTransactionConnection, tenantId: string, relation: SeedingRelation ) => - createItem({ + createOrUpdateItem({ transaction, tableName: RolesScopes.table, tenantId, toLogFieldName: 'role_id', - whereClauses: [sql`role_id = ${relation.role_id}`, sql`scope_id = ${relation.scope_id}`], + whereClauses: [ + sql`tenant_id = ${tenantId}`, + sql`role_id = ${relation.role_id}`, + sql`scope_id = ${relation.scope_id}`, + ], toInsert: relation, }); @@ -165,74 +175,43 @@ const createRelations = async (params: { }) => { const relationsToCrete: SeedingRelation[] = []; - params.roles.forEach((role) => { - role.scopes.forEach((scopeGroup) => { + for (const role of params.roles) { + for (const scopeGroup of role.scopes) { const relations = scopeGroup.specific_permissions.map((permission) => { - // @ts-ignore @typescript-eslint/no-non-null-asserted-optional-chain if (params.scopes[scopeGroup.resource_id]?.[permission]?.id === undefined) { - throw new Error("Requested permission does not exist.") + throw new Error('Requested permission does not exist.'); } return { - role_id: role.id!, - // @ts-ignore @typescript-eslint/no-non-null-asserted-optional-chain - scope_id: params.scopes[scopeGroup.resource_id]?.[permission]?.id! - } + role_id: role.id, + scope_id: params.scopes[scopeGroup.resource_id]?.[permission]?.id!, + }; }); relationsToCrete.push(...relations); - }) - }); + } + } - const queries = relationsToCrete.map((relation) => - createRoleScopeRelation( - params.transaction, - params.tenantId, - relation - ) + const queries = relationsToCrete.map(async (relation) => + createRoleScopeRelation(params.transaction, params.tenantId, relation) ); await Promise.all(queries); return relationsToCrete; -} - -const replaceWithResourceIdFromDatabase = ( - seededResources: Record, - toSeed: { - resource_permissions?: ResourcePermissionSeeder[]; - resource_roles?: ResourceRoleSeeder[]; - } -): { - resource_permissions?: ResourcePermissionSeeder[]; - resource_roles?: ResourceRoleSeeder[]; -} => { - if (toSeed.resource_permissions?.length) { - for (const permission of toSeed.resource_permissions) { - const seededResourceId = seededResources[permission.resource_id]; - if (!seededResourceId) { - throw new Error( - `Resource scopes. Referring to a not existent resource id: ${permission.resource_id}!` - ); - } - permission.resource_id = seededResourceId.id!; - } - } +}; - if (toSeed.resource_roles?.length) { - for (const roles of toSeed.resource_roles) { - for (const permissionGroup of roles.permissions) { - const seededResourceId = seededResources[permissionGroup.resource_id]; - if (!seededResourceId) { - throw new Error( - `Resource roles. Referring to a not existent resource id: ${permissionGroup.resource_id}!` - ); - } - permissionGroup.resource_id = seededResourceId.id!; - } - } - } +export const cleanScopes = async (transaction: DatabaseTransactionConnection, tenantId: string) => { + await cleanScopeRelations(transaction, tenantId); + const deleteQueryString = deleteQuery([sql`tenant_id = ${tenantId}`], Scopes.table); + return transaction.query(deleteQueryString); +}; - return toSeed; +export const cleanScopeRelations = async ( + transaction: DatabaseTransactionConnection, + tenantId: string +) => { + const deleteQueryString = deleteQuery([sql`tenant_id = ${tenantId}`], RolesScopes.table); + return transaction.query(deleteQueryString); }; export const seedResourceRbacData = async (params: { @@ -244,12 +223,13 @@ export const seedResourceRbacData = async (params: { resource_roles?: ResourceRoleSeeder[]; }; }) => { - params.toSeed = replaceWithResourceIdFromDatabase(params.seededResources, params.toSeed); if (params.toSeed.resource_permissions?.length && params.toSeed.resource_roles?.length) { + await cleanScopes(params.transaction, params.tenantId); + const createdScopes = await createScopes({ transaction: params.transaction, tenantId: params.tenantId, - scopesToSeed: params.toSeed.resource_permissions + scopesToSeed: params.toSeed.resource_permissions, }); const createdRoles = await createRoles({ @@ -263,7 +243,7 @@ export const seedResourceRbacData = async (params: { transaction: params.transaction, tenantId: params.tenantId, roles: createdRoles, - scopes: createdScopes + scopes: createdScopes, }); } }; diff --git a/packages/cli/src/commands/database/ogcio/resources.ts b/packages/cli/src/commands/database/ogcio/resources.ts index 539db60f12c..ffd386592f0 100644 --- a/packages/cli/src/commands/database/ogcio/resources.ts +++ b/packages/cli/src/commands/database/ogcio/resources.ts @@ -6,19 +6,19 @@ import { Resources } from '@logto/schemas'; import { sql, type DatabaseTransactionConnection } from '@silverhand/slonik'; import { type ResourceSeeder } from './ogcio-seeder.js'; -import { createItem } from './queries.js'; +import { createOrUpdateItem } from './queries.js'; const createResource = async ( transaction: DatabaseTransactionConnection, tenantId: string, appToSeed: SeedingResource ) => - createItem({ + createOrUpdateItem({ transaction, tenantId, toInsert: appToSeed, toLogFieldName: 'name', - whereClauses: [sql`indicator = ${appToSeed.indicator}`], + whereClauses: [sql`tenant_id = ${tenantId}`, sql`id = ${appToSeed.id}`], tableName: Resources.table, }); @@ -26,18 +26,14 @@ const setResourceId = async ( element: SeedingResource, transaction: DatabaseTransactionConnection, tenantId: string -): Promise< - Omit & { - id: string; - } -> => { +): Promise => { const outputValue = await createResource(transaction, tenantId, element); return outputValue; }; export type SeedingResource = { - id?: string; + id: string; name: string; indicator: string; is_default?: boolean; @@ -45,6 +41,7 @@ export type SeedingResource = { }; const fillResource = (resourceSeeder: ResourceSeeder): SeedingResource => ({ + id: resourceSeeder.id, name: resourceSeeder.name, indicator: resourceSeeder.indicator, is_default: false, diff --git a/packages/cli/src/commands/database/ogcio/webhooks.ts b/packages/cli/src/commands/database/ogcio/webhooks.ts index 407cc1e972e..096fef61f39 100644 --- a/packages/cli/src/commands/database/ogcio/webhooks.ts +++ b/packages/cli/src/commands/database/ogcio/webhooks.ts @@ -5,7 +5,7 @@ import { Hooks } from '@logto/schemas'; import { sql, type DatabaseTransactionConnection } from '@silverhand/slonik'; import { type WebhookSeeder } from './ogcio-seeder.js'; -import { createItemWithoutId } from './queries.js'; +import { createOrUpdateItemWithoutId } from './queries.js'; type SeedingWebhook = { tenant_id: string; @@ -22,20 +22,17 @@ const createWebhook = async ( tenantId: string, webhookToSeed: SeedingWebhook ) => - createItemWithoutId({ + createOrUpdateItemWithoutId({ transaction, tenantId, toInsert: webhookToSeed, toLogFieldName: 'id', - whereClauses: [sql`id = ${webhookToSeed.id}`], + whereClauses: [sql`tenant_id = ${tenantId}`, sql`id = ${webhookToSeed.id}`], tableName: Hooks.table, columnToGet: 'id', }); -const fillWebhooks = ( - inputHooks: WebhookSeeder[], - tenantId: string -): SeedingWebhook[] => { +const fillWebhooks = (inputHooks: WebhookSeeder[], tenantId: string): SeedingWebhook[] => { return inputHooks.map((hook) => ({ tenant_id: tenantId, id: hook.id, @@ -43,7 +40,7 @@ const fillWebhooks = ( events: JSON.stringify(hook.events), config: JSON.stringify(hook.config), signing_key: hook.signing_key, - enabled: hook.enabled + enabled: hook.enabled, })); }; diff --git a/packages/core/src/libraries/ogcio-constants.ts b/packages/core/src/libraries/ogcio-constants.ts new file mode 100644 index 00000000000..9fd839f244a --- /dev/null +++ b/packages/core/src/libraries/ogcio-constants.ts @@ -0,0 +1,12 @@ +export const OGCIO_ORGANIZATIONS = { + OGCIO: 'ogcio', +}; + +export const OGCIO_ORGANIZATION_ROLES = { + BB_PUBLIC_SERVANT: 'bb-public-servant', + MSG_PUBLIC_SERVANT: 'msg-public-servant', +}; + +export const OGCIO_ROLES = { + BB_CITIZEN: 'bb-citizen', +}; diff --git a/packages/core/src/libraries/ogcio-user.ts b/packages/core/src/libraries/ogcio-user.ts index d916acff866..0dbc601d36a 100644 --- a/packages/core/src/libraries/ogcio-user.ts +++ b/packages/core/src/libraries/ogcio-user.ts @@ -16,6 +16,8 @@ import { EnvSet } from '#src/env-set/index.js'; import type OrganizationQueries from '#src/queries/organization/index.js'; import assertThat from '#src/utils/assert-that.js'; +import { OGCIO_ORGANIZATION_ROLES, OGCIO_ORGANIZATIONS, OGCIO_ROLES } from './ogcio-constants.js'; + const getDefaultOrganizationsForUser = async (organizationQueries: OrganizationQueries) => { const organizationNames: string[] = deduplicate(EnvSet.values.userDefaultOrganizationNames); consoleLog.info('DEFUALT ORG NAMES', organizationNames); @@ -136,15 +138,10 @@ const getDomainFromEmail = (email: string): string | undefined => { const assignCitizenRole = async ( user: User, - getRoles: (roleName: string, excludeRoleId?: string) => Promise, + getRoles: (id: string) => Promise, insertUsersRoles: (usersRoles: CreateUsersRole[]) => Promise> ) => { - const userRole = await getRoles('Citizen'); - - if (userRole === undefined) { - consoleLog.error(phrases.en.errors.role.default_role_missing); - return; - } + const userRole = await getRoles(OGCIO_ROLES.BB_CITIZEN); return insertUsersRoles([ { @@ -158,7 +155,7 @@ const assignCitizenRole = async ( const assignUserToOrganization = async (user: User, organizationQueries: OrganizationQueries) => { try { - const organization = await organizationQueries.findById('ogcio'); + const organization = await organizationQueries.findById(OGCIO_ORGANIZATIONS.OGCIO); await organizationQueries.relations.users.insert([organization.id, user.id]); return organization; } catch { @@ -171,13 +168,9 @@ const assignOrganizationRoleToUser = async ( organization: Organization, organizationQueries: OrganizationQueries ) => { - const allOrganizationRoles = await organizationQueries.roles.findAll(100, 0); - const publicServantRole = allOrganizationRoles[1].find((role) => role.name === 'Public Servant'); - - if (publicServantRole === undefined) { - consoleLog.error(phrases.en.errors.role.default_role_missing); - return; - } + const publicServantRole = await organizationQueries.roles.findById( + OGCIO_ORGANIZATION_ROLES.BB_PUBLIC_SERVANT + ); await organizationQueries.relations.rolesUsers.insert([ organization.id, @@ -198,7 +191,7 @@ const assignPublicServantRole = async (user: User, organizationQueries: Organiza export const manageDefaultUserRole = async ( user: User, - getRoles: (roleName: string, excludeRoleId?: string) => Promise, + getRoles: (id: string) => Promise, insertUsersRoles: (usersRoles: CreateUsersRole[]) => Promise>, organizationQueries: OrganizationQueries ) => { diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 988da35cd0e..8574a117b6f 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -195,8 +195,7 @@ async function handleSubmitRegister( // OGCIO await manageDefaultUserRole( user, - // @ts-expect-error: strange error in roles.findRoleByRoleName return type - roles.findRoleByRoleName, + roles.findRoleById, usersRoles.insertUsersRoles, organizations );