diff --git a/examples/directory/custom-domains/custom-domains.json b/examples/directory/custom-domains/custom-domains.json new file mode 100644 index 00000000..5301be7b --- /dev/null +++ b/examples/directory/custom-domains/custom-domains.json @@ -0,0 +1,10 @@ +[ + { + "domain": "my_domain.com", + "type": "auth0_managed_certs", + "tls_policy": "recommended", + "domain_metadata": { + "myKey": "value" + } + } +] \ No newline at end of file diff --git a/examples/yaml/tenant.yaml b/examples/yaml/tenant.yaml index 234a83f6..99821d89 100644 --- a/examples/yaml/tenant.yaml +++ b/examples/yaml/tenant.yaml @@ -272,3 +272,11 @@ networkACLs: scope: 'authentication' match: geo_country_codes: ['US', 'CA'] + +customDomains: + - domain: my_domain.com + type: auth0_managed_certs + tls_policy: 'recommended' + domain_metadata: + myKey: value + diff --git a/src/tools/auth0/handlers/customDomains.ts b/src/tools/auth0/handlers/customDomains.ts index 19954165..4e33d27b 100644 --- a/src/tools/auth0/handlers/customDomains.ts +++ b/src/tools/auth0/handlers/customDomains.ts @@ -1,6 +1,7 @@ import { CustomDomain } from 'auth0'; import DefaultAPIHandler, { order } from './default'; import { Asset, Assets } from '../../../types'; +import log from '../../../logger'; export const schema = { type: 'array', @@ -18,6 +19,22 @@ export const schema = { status: { type: 'string', enum: ['pending_verification', 'ready', 'disabled', 'pending'] }, type: { type: 'string', enum: ['auth0_managed_certs', 'self_managed_certs'] }, verification: { type: 'object' }, + tls_policy: { + type: 'string', + description: 'Custom domain TLS policy. Must be `recommended`, includes TLS 1.2.', + defaultValue: 'recommended', + }, + domain_metadata: { + type: 'object', + description: 'Domain metadata associated with the custom domain.', + defaultValue: undefined, + maxProperties: 10, + }, + verification_method: { + type: 'string', + description: 'Custom domain verification method. Must be `txt`.', + defaultValue: 'txt', + }, }, required: ['domain', 'type'], }, @@ -31,10 +48,21 @@ export default class CustomDomainsHadnler extends DefaultAPIHandler { ...config, type: 'customDomains', id: 'custom_domain_id', - identifiers: ['domain'], - stripCreateFields: ['status', 'primary', 'verification'], + identifiers: ['custom_domain_id', 'domain'], + stripCreateFields: ['status', 'primary', 'verification', 'certificate'], + stripUpdateFields: [ + 'status', + 'primary', + 'verification', + 'type', + 'domain', + 'verification_method', + 'certificate', + ], functions: { delete: (args) => this.client.customDomains.delete({ id: args.custom_domain_id }), + update: (args, data) => + this.client.customDomains.update({ id: args.custom_domain_id }, data), }, }); } @@ -71,29 +99,21 @@ export default class CustomDomainsHadnler extends DefaultAPIHandler { const { customDomains } = assets; if (!customDomains) return; - const changes = await this.calcChanges(assets).then((changes) => { - const changesWithoutUpdates = { - ...changes, - create: changes.create.map((customDomainToCreate) => { - const newCustomDomain = { ...customDomainToCreate }; - if (customDomainToCreate.custom_client_ip_header === null) { - delete newCustomDomain.custom_client_ip_header; - } - return newCustomDomain; - }), - delete: changes.del.map((deleteToMake) => { - const deleteWithSDKCompatibleID = { - ...deleteToMake, - id: deleteToMake.custom_domain_id, - }; - delete deleteWithSDKCompatibleID['custom_domain_id']; - return deleteWithSDKCompatibleID; - }), - update: [], //Do not perform custom domain updates because not supported by SDK - }; - return changesWithoutUpdates; - }); + // Deprecation warnings for custom domains + if (customDomains.some((customDomain) => customDomain.primary != null)) { + log.warn( + 'The "primary" field is deprecated and may be removed in future versions for "customDomains"' + ); + } + + if (customDomains.some((customDomain) => 'verification_method' in customDomain)) { + log.warn( + 'The "verification_method" field is deprecated and may be removed in future versions for "customDomains"' + ); + } + + const changes = await this.calcChanges(assets); await super.processChanges(assets, changes); } diff --git a/test/tools/auth0/handlers/customDomains.test.ts b/test/tools/auth0/handlers/customDomains.test.ts index e6a89ef9..793afdce 100644 --- a/test/tools/auth0/handlers/customDomains.test.ts +++ b/test/tools/auth0/handlers/customDomains.test.ts @@ -37,7 +37,7 @@ describe('#customDomains handler', () => { }), }; - //@ts-ignore + // @ts-ignore const handler = new customDomainsHandler({ client: auth0ApiClientMock }); const data = await handler.load(); @@ -65,7 +65,7 @@ describe('#customDomains handler', () => { }), }; - //@ts-ignore + // @ts-ignore const handler = new customDomainsHandler({ client: auth0ApiClientMock }); const data = await handler.getType(); @@ -95,7 +95,7 @@ describe('#customDomains handler', () => { }), }; - //@ts-ignore + // @ts-ignore const handler = new customDomainsHandler({ client: auth0ApiClientMock }); const data = await handler.load(); @@ -133,7 +133,7 @@ describe('#customDomains handler', () => { }), }; - //@ts-ignore + // @ts-ignore const handler = new customDomainsHandler({ config: () => {}, client: auth0ApiClientMock as unknown as Auth0APIClient, @@ -173,7 +173,7 @@ describe('#customDomains handler', () => { }), }; - //@ts-ignore + // @ts-ignore const handler = new customDomainsHandler({ config: () => {}, client: auth0ApiClientMock as unknown as Auth0APIClient, @@ -210,7 +210,7 @@ describe('#customDomains handler', () => { }), }; - //@ts-ignore + // @ts-ignore const handler = new customDomainsHandler({ config: (key) => { return { AUTH0_ALLOW_DELETE: true }[key]; @@ -219,7 +219,7 @@ describe('#customDomains handler', () => { }); await handler.processChanges({ customDomains: [] }); - expect(didUpdateFunctionGetCalled).to.equal(false); //The update function should not be called + expect(didUpdateFunctionGetCalled).to.equal(false); // The update function should not be called expect(didDeleteFunctionGetCalled).to.equal(true); expect(didCreateFunctionGetCalled).to.equal(false); }); @@ -249,7 +249,7 @@ describe('#customDomains handler', () => { }), }; - //@ts-ignore + // @ts-ignore const handler = new customDomainsHandler({ config: (key) => { return { AUTH0_ALLOW_DELETE: false }[key]; @@ -258,7 +258,7 @@ describe('#customDomains handler', () => { }); await handler.processChanges({ customDomains: [] }); - expect(didUpdateFunctionGetCalled).to.equal(false); //The update function should not be called + expect(didUpdateFunctionGetCalled).to.equal(false); // The update function should not be called expect(didDeleteFunctionGetCalled).to.equal(false); expect(didCreateFunctionGetCalled).to.equal(false); }); @@ -288,15 +288,266 @@ describe('#customDomains handler', () => { }), }; - //@ts-ignore + // @ts-ignore const handler = new customDomainsHandler({ config: () => {}, client: auth0ApiClientMock as unknown as Auth0APIClient, }); await handler.processChanges({ customDomains: [] }); - expect(didUpdateFunctionGetCalled).to.equal(false); //The update function should not be called + expect(didUpdateFunctionGetCalled).to.equal(false); // The update function should not be called expect(didDeleteFunctionGetCalled).to.equal(false); expect(didCreateFunctionGetCalled).to.equal(false); }); + + // Update Functionality Tests + it('should update custom domains when changes exist', async () => { + let didUpdateFunctionGetCalled = false; + + const existingCustomDomain = { + custom_domain_id: 'cd_123', + domain: 'existing.example.com', + type: 'auth0_managed_certs', + status: 'ready', + tls_policy: 'recommended', + }; + + const updatedCustomDomain = { + custom_domain_id: 'cd_123', + domain: 'existing.example.com', + type: 'auth0_managed_certs', + tls_policy: 'recommended', + domain_metadata: { environment: 'production' }, + }; + + const auth0ApiClientMock = { + customDomains: { + getAll: async () => ({ data: [existingCustomDomain] }), + create: async () => {}, + update: async (args, data) => { + didUpdateFunctionGetCalled = true; + expect(args).to.deep.equal({ id: 'cd_123' }); + expect(data).to.deep.equal({ + tls_policy: 'recommended', + domain_metadata: { environment: 'production' }, + }); + return { data: updatedCustomDomain }; + }, + delete: async () => {}, + }, + pool: new PromisePoolExecutor({ + concurrencyLimit: 3, + frequencyLimit: 8, + frequencyWindow: 1000, // 1 sec + }), + }; + + // @ts-ignore + const handler = new customDomainsHandler({ + config: () => {}, + client: auth0ApiClientMock as unknown as Auth0APIClient, + }); + + await handler.processChanges({ customDomains: [updatedCustomDomain] }); + + expect(didUpdateFunctionGetCalled).to.equal(true); + }); + + it('should call update with correct parameters and strip update fields', async () => { + let updateCallData = null; + + const existingCustomDomain = { + domain: 'test.example.com', + type: 'auth0_managed_certs', + status: 'ready', + }; + + const updatedCustomDomainWithExtraFields = { + domain: 'test.example.com', // should be stripped + type: 'auth0_managed_certs', // should be stripped + status: 'ready', // should be stripped + primary: true, // should be stripped + verification: { method: 'cname' }, // should be stripped + verification_method: 'cname', // should be stripped + tls_policy: 'recommended', // should NOT be stripped + domain_metadata: { key: 'value' }, // should NOT be stripped + }; + + const auth0ApiClientMock = { + customDomains: { + getAll: async () => ({ data: [existingCustomDomain] }), + create: async () => {}, + update: async (args, data) => { + updateCallData = data; + return { data: {} }; + }, + delete: async () => {}, + }, + pool: new PromisePoolExecutor({ + concurrencyLimit: 3, + frequencyLimit: 8, + frequencyWindow: 1000, // 1 sec + }), + }; + + // @ts-ignore + const handler = new customDomainsHandler({ + config: () => {}, + client: auth0ApiClientMock as unknown as Auth0APIClient, + }); + + await handler.processChanges({ customDomains: [updatedCustomDomainWithExtraFields] }); + + // Verify that stripped fields are not present + expect(updateCallData).to.not.have.property('domain'); + expect(updateCallData).to.not.have.property('type'); + expect(updateCallData).to.not.have.property('status'); + expect(updateCallData).to.not.have.property('primary'); + expect(updateCallData).to.not.have.property('verification'); + expect(updateCallData).to.not.have.property('verification_method'); + + // Verify that allowed fields are present + expect(updateCallData).to.have.property('tls_policy', 'recommended'); + expect(updateCallData).to.have.property('domain_metadata'); + expect(updateCallData.domain_metadata).to.deep.equal({ key: 'value' }); + }); + + // New Schema Fields Tests + it('should handle domain_metadata in custom domains', async () => { + let didCreateFunctionGetCalled = false; + let createCallArgs = null; + + const customDomainWithMetadata = { + domain: 'metadata.example.com', + type: 'auth0_managed_certs', + domain_metadata: { + environment: 'test', + team: 'engineering', + cost_center: '12345', + }, + }; + + const auth0ApiClientMock = { + customDomains: { + getAll: async () => ({ data: [] }), + create: async (args) => { + didCreateFunctionGetCalled = true; + createCallArgs = args; + return { data: customDomainWithMetadata }; + }, + update: async () => {}, + delete: async () => {}, + }, + pool: new PromisePoolExecutor({ + concurrencyLimit: 3, + frequencyLimit: 8, + frequencyWindow: 1000, // 1 sec + }), + }; + + // @ts-ignore + const handler = new customDomainsHandler({ + config: () => {}, + client: auth0ApiClientMock as unknown as Auth0APIClient, + }); + + await handler.processChanges({ customDomains: [customDomainWithMetadata] }); + + expect(didCreateFunctionGetCalled).to.equal(true); + expect(createCallArgs).to.have.property('domain_metadata'); + expect(createCallArgs.domain_metadata).to.deep.equal({ + environment: 'test', + team: 'engineering', + cost_center: '12345', + }); + }); + + it('should create custom domains with tls_policy field', async () => { + let didCreateFunctionGetCalled = false; + let createCallArgs = null; + + const customDomainWithTlsPolicy = { + domain: 'tls.example.com', + type: 'auth0_managed_certs', + tls_policy: 'recommended', + }; + + const auth0ApiClientMock = { + customDomains: { + getAll: async () => ({ data: [] }), + create: async (args) => { + didCreateFunctionGetCalled = true; + createCallArgs = args; + return { data: customDomainWithTlsPolicy }; + }, + update: async () => {}, + delete: async () => {}, + }, + pool: new PromisePoolExecutor({ + concurrencyLimit: 3, + frequencyLimit: 8, + frequencyWindow: 1000, // 1 sec + }), + }; + + // @ts-ignore + const handler = new customDomainsHandler({ + config: () => {}, + client: auth0ApiClientMock as unknown as Auth0APIClient, + }); + + await handler.processChanges({ customDomains: [customDomainWithTlsPolicy] }); + + expect(didCreateFunctionGetCalled).to.equal(true); + expect(createCallArgs).to.have.property('tls_policy', 'recommended'); + }); + + it('should support both tls_policy and domain_metadata together', async () => { + let didCreateFunctionGetCalled = false; + let createCallArgs = null; + + const customDomainWithBothFields = { + domain: 'combined.example.com', + type: 'auth0_managed_certs', + tls_policy: 'recommended', + domain_metadata: { + environment: 'production', + owner: 'platform-team', + }, + }; + + const auth0ApiClientMock = { + customDomains: { + getAll: async () => ({ data: [] }), + create: async (args) => { + didCreateFunctionGetCalled = true; + createCallArgs = args; + return { data: customDomainWithBothFields }; + }, + update: async () => {}, + delete: async () => {}, + }, + pool: new PromisePoolExecutor({ + concurrencyLimit: 3, + frequencyLimit: 8, + frequencyWindow: 1000, // 1 sec + }), + }; + + // @ts-ignore + const handler = new customDomainsHandler({ + config: () => {}, + client: auth0ApiClientMock as unknown as Auth0APIClient, + }); + + await handler.processChanges({ customDomains: [customDomainWithBothFields] }); + + expect(didCreateFunctionGetCalled).to.equal(true); + expect(createCallArgs).to.have.property('tls_policy', 'recommended'); + expect(createCallArgs).to.have.property('domain_metadata'); + expect(createCallArgs.domain_metadata).to.deep.equal({ + environment: 'production', + owner: 'platform-team', + }); + }); });