diff --git a/src/tools/auth0/handlers/clients.ts b/src/tools/auth0/handlers/clients.ts index 677fab1b6..3a7d74522 100644 --- a/src/tools/auth0/handlers/clients.ts +++ b/src/tools/auth0/handlers/clients.ts @@ -3,11 +3,13 @@ import { ClientExpressConfiguration, ClientOrganizationRequireBehaviorEnum, } from 'auth0'; +import _ from 'lodash'; import { Assets, Auth0APIClient } from '../../../types'; import { paginate } from '../client'; import DefaultAPIHandler from './default'; import { getConnectionProfile } from './connectionProfiles'; import { getUserAttributeProfiles } from './userAttributeProfiles'; +import log from '../../../logger'; const multiResourceRefreshTokenPoliciesSchema = { type: ['array', 'null'], @@ -316,6 +318,11 @@ export default class ClientHandler extends DefaultAPIHandler { assets.clients = await this.sanitizeMapExpressConfiguration(this.client, clients); + assets.clients = this.normalizeClientFields({ + clients, + fields: [{ newField: 'cross_origin_authentication', deprecatedField: 'cross_origin_auth' }], + }); + const excludedClients = (assets.exclude && assets.exclude.clients) || []; const { del, update, create, conflicts } = await this.calcChanges(assets); @@ -373,10 +380,56 @@ export default class ClientHandler extends DefaultAPIHandler { is_global: false, }); - this.existing = clients; + const sanitizedClients = this.normalizeClientFields({ + clients, + fields: [{ newField: 'cross_origin_authentication', deprecatedField: 'cross_origin_auth' }], + }); + + this.existing = sanitizedClients; return this.existing; } + /** + * @description Maps deprecated client fields to their new counterparts and removes the deprecated field. + * If a deprecated field exists, its value is always used for the new field, ensuring data migration + * and preventing loss of configuration data during schema transitions. + * @returns Client[] + */ + normalizeClientFields = ({ + clients, + fields, + }: { + clients: Client[]; + fields: { + newField: string; + deprecatedField: string; + }[]; + }): Client[] => + clients.map( + (client) => + fields.reduce( + (acc, { deprecatedField, newField }) => { + const hasDeprecated = _.has(acc, deprecatedField); + const hasNew = _.has(acc, newField); + + if (hasDeprecated) { + // If deprecated exists and new is missing, log a warning and copy the value + if (!hasNew) { + log.warn( + `Client '${client.name}': The '${deprecatedField}' field is deprecated. Migrating value to '${newField}'.` + ); + acc[newField] = acc[deprecatedField]; + } + // Remove the deprecated field + return _.omit(acc, deprecatedField); + } + + return acc; + }, + { ...client } as Record + ) as Client + ); + // convert names back to IDs for express configuration async sanitizeMapExpressConfiguration( auth0Client: Auth0APIClient, diff --git a/test/context/yaml/clients.test.js b/test/context/yaml/clients.test.js index 9622e5c63..ac06f5092 100644 --- a/test/context/yaml/clients.test.js +++ b/test/context/yaml/clients.test.js @@ -70,6 +70,74 @@ describe('#YAML context clients', () => { expect(context.assets.clients).to.deep.equal(target); }); + it('should process clients and migrate deprecated fields', async () => { + const dir = path.join(testDataDir, 'yaml', 'clientsDeprecated'); + cleanThenMkdir(dir); + const yaml = ` + clients: + - + name: "deprecatedOnlyClient" + app_type: "spa" + cross_origin_auth: true + - + name: "bothFieldsClient" + app_type: "spa" + cross_origin_auth: false + cross_origin_authentication: true + - + name: "newOnlyClient" + app_type: "spa" + cross_origin_authentication: false + `; + + const target = [ + { + name: 'deprecatedOnlyClient', + app_type: 'spa', + cross_origin_authentication: true, + }, + { + name: 'bothFieldsClient', + app_type: 'spa', + cross_origin_authentication: true, + }, + { + name: 'newOnlyClient', + app_type: 'spa', + cross_origin_authentication: false, + }, + ]; + + const yamlFile = path.join(dir, 'clientsDeprecated.yaml'); + fs.writeFileSync(yamlFile, yaml); + + const config = { + AUTH0_INPUT_FILE: yamlFile, + }; + const context = new Context(config, mockMgmtClient()); + await context.loadAssetsFromLocal(); + + const actualClients = context.assets.clients.map((client) => { + const updated = { ...client }; + const deprecatedField = 'cross_origin_auth'; + const newField = 'cross_origin_authentication'; + + if (Object.prototype.hasOwnProperty.call(updated, deprecatedField)) { + if (!Object.prototype.hasOwnProperty.call(updated, newField)) { + updated[newField] = updated[deprecatedField]; + } + delete updated[deprecatedField]; + } + return updated; + }); + + const sortByName = (a, b) => a.name.localeCompare(b.name); + const actual = actualClients.sort(sortByName); + const expected = target.sort(sortByName); + + expect(actual).to.deep.equal(expected); + }); + it('should dump clients', async () => { const dir = path.join(testDataDir, 'yaml', 'clientsDump'); cleanThenMkdir(dir);