Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions src/tools/auth0/handlers/guardianFactorTemplates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,22 @@ export default class GuardianFactorTemplatesHandler extends DefaultHandler {
guardianFactorTemplates.map(async (fatorTemplates) => {
const { name, ...data } = fatorTemplates;
const params = { name: fatorTemplates.name };
if (name === 'sms') {
await this.client.guardian.factors.sms.setTemplates(
data as Management.SetGuardianFactorSmsTemplatesRequestContent
);
} else if (name === 'phone') {
await this.client.guardian.factors.phone.setTemplates(
data as Management.SetGuardianFactorPhoneTemplatesRequestContent
);
try {
if (name === 'sms') {
await this.client.guardian.factors.sms.setTemplates(
data as Management.SetGuardianFactorSmsTemplatesRequestContent
);
} else if (name === 'phone') {
await this.client.guardian.factors.phone.setTemplates(
data as Management.SetGuardianFactorPhoneTemplatesRequestContent
);
}
} catch (err) {
if (isForbiddenFeatureError(err, this.type)) {
// Feature is deprecated/disabled on this tenant; warn and skip instead of failing the import.
return;
}
throw err;
}
this.didUpdate(params);
this.updated += 1;
Expand Down
25 changes: 12 additions & 13 deletions src/tools/auth0/handlers/guardianPhoneFactorMessageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,7 @@ const isFeatureUnavailableError = (err): boolean => {
// Older Management API version where the endpoint is not available.
return true;
}
if (
err.statusCode === 403 &&
err.originalError &&
err.originalError.response &&
err.originalError.response.body &&
err.originalError.response.body.errorCode === 'voice_mfa_not_allowed'
) {
// Recent Management API version, but with feature explicitly disabled.
return true;
}
// 403s (feature explicitly disabled) are handled by isForbiddenFeatureError.
return false;
};

Expand Down Expand Up @@ -84,9 +75,17 @@ export default class GuardianPhoneMessageTypesHandler extends DefaultHandler {
}

const data = guardianPhoneFactorMessageTypes;
await this.client.guardian.factors.phone.setMessageTypes(
data as unknown as Management.SetGuardianFactorPhoneMessageTypesRequestContent
);
try {
await this.client.guardian.factors.phone.setMessageTypes(
data as unknown as Management.SetGuardianFactorPhoneMessageTypesRequestContent
);
} catch (err) {
if (isFeatureUnavailableError(err) || isForbiddenFeatureError(err, this.type)) {
// Feature is deprecated/disabled on this tenant; warn and skip instead of failing the import.
return;
}
throw err;
}
this.updated += 1;
this.didUpdate(guardianPhoneFactorMessageTypes);
}
Expand Down
25 changes: 12 additions & 13 deletions src/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,7 @@ const isFeatureUnavailableError = (err) => {
// Older Management API version where the endpoint is not available.
return true;
}
if (
err.statusCode === 403 &&
err.originalError &&
err.originalError.response &&
err.originalError.response.body &&
err.originalError.response.body.errorCode === 'hooks_not_allowed'
) {
// Recent Management API version, but with feature explicitly disabled.
return true;
}
// 403s (feature explicitly disabled) are handled by isForbiddenFeatureError.
return false;
};

Expand Down Expand Up @@ -81,9 +72,17 @@ export default class GuardianPhoneSelectedProviderHandler extends DefaultHandler
}

const data = guardianPhoneFactorSelectedProvider;
await this.client.guardian.factors.phone.setProvider(
data as Management.SetGuardianFactorsProviderPhoneRequestContent
);
try {
await this.client.guardian.factors.phone.setProvider(
data as Management.SetGuardianFactorsProviderPhoneRequestContent
);
} catch (err) {
if (isFeatureUnavailableError(err) || isForbiddenFeatureError(err, this.type)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, curious about the hooks_not_allowed errorCode here, is this specific to the phone provider endpoint, or could it have been carried over from somewhere else?

Asking because I'd expect Auth0 to return something like legacy_mfa_phone_provider_not_allowed here rather than a hooks related errorCode

If it's not needed, we could simplify isFeatureUnavailableError to just handle the 404 case and let isForbiddenFeatureError take care of 403s (which it already does)

Totally a nit though, not a blocker!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! you're right. hooks_not_allowed was pre-existing and redundant since isForbiddenFeatureError already handles any 403. Simplified isFeatureUnavailableError to just the 404 case in both handlers. Pushed the change 👍

// Feature is deprecated/disabled on this tenant; warn and skip instead of failing the import.
return;
}
throw err;
}
this.updated += 1;
this.didUpdate(guardianPhoneFactorSelectedProvider);
}
Expand Down
7 changes: 6 additions & 1 deletion src/tools/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,12 @@ export const isDeprecatedError = (err: { message: string; statusCode: number }):

export const isForbiddenFeatureError = (err, type): boolean => {
if (err.statusCode === 403) {
log.warn(`${err.message};${err.errorCode ?? ''} - Skipping ${type}`);
// The SDK error's top-level `message` is the full serialized response body; the clean,
// human-readable message and errorCode live on the response body itself.
const body = err.originalError?.response?.body ?? {};
const message = body.message ?? err.message;
const errorCode = body.errorCode ?? err.errorCode ?? '';
log.warn(`${message}${errorCode ? ` (${errorCode})` : ''} - Skipping ${type}`);
return true;
}
return false;
Expand Down
33 changes: 33 additions & 0 deletions test/tools/auth0/handlers/guardianFactorTemplates.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,38 @@ describe('#guardianFactorTemplates handler', () => {

await stageFn.apply(handler, [{ guardianFactorTemplates: data }]);
});

it('should warn and skip when the legacy feature is not allowed on the tenant', async () => {
const auth0 = {
guardian: {
factors: {
sms: {
setTemplates: () => {
const err = new Error(
'Insufficient privileges to use this deprecated feature. To handle Phone Provider/Templates refer to Tenant Phone Settings'
);
err.statusCode = 403;
err.errorCode = 'legacy_mfa_phone_provider_not_allowed';
return Promise.reject(err);
},
},
},
},
pool,
};

const handler = new guardianFactorTemplatesTests.default({ client: auth0, config });
const stageFn = Object.getPrototypeOf(handler).processChanges;
const data = [
{
name: 'sms',
enrollment_message: 'test',
},
];

// Should resolve (not throw) even though the API rejects with a 403.
await stageFn.apply(handler, [{ guardianFactorTemplates: data }]);
expect(handler.updated).to.equal(0);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,31 @@ describe('#guardianPhoneFactorMessageTypes handler', () => {

await stageFn.apply(handler, [{ guardianPhoneFactorMessageTypes: null }]);
});

it('should warn and skip when the legacy feature is not allowed on the tenant', async () => {
const auth0 = {
guardian: {
factors: {
phone: {
setMessageTypes: () => {
const err = new Error('This endpoint is disabled for your tenant.');
err.statusCode = 403;
err.errorCode = 'voice_mfa_not_allowed';
return Promise.reject(err);
},
},
},
},
};

const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 });
const stageFn = Object.getPrototypeOf(handler).processChanges;

// Should resolve (not throw) even though the API rejects with a 403.
await stageFn.apply(handler, [
{ guardianPhoneFactorMessageTypes: { message_types: ['sms', 'voice'] } },
]);
expect(handler.updated).to.equal(0);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,31 @@ describe('#guardianPhoneFactorSelectedProvider handler', () => {

await stageFn.apply(handler, [{ guardianPhoneFactorSelectedProvider: null }]);
});

it('should warn and skip when the legacy feature is not allowed on the tenant', async () => {
const auth0 = {
guardian: {
factors: {
phone: {
setProvider: () => {
const err = new Error('This endpoint is disabled for your tenant.');
err.statusCode = 403;
err.errorCode = 'legacy_mfa_phone_provider_not_allowed';
return Promise.reject(err);
},
},
},
},
};

const handler = new guardianPhoneFactorSelectedProvider.default({ client: auth0 });
const stageFn = Object.getPrototypeOf(handler).processChanges;

// Should resolve (not throw) even though the API rejects with a 403.
await stageFn.apply(handler, [
{ guardianPhoneFactorSelectedProvider: { provider: 'twilio' } },
]);
expect(handler.updated).to.equal(0);
});
});
});
41 changes: 39 additions & 2 deletions test/tools/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,7 @@ describe('#isForbiddenFeatureError', () => {

// eslint-disable-next-line no-unused-expressions
expect(utils.isForbiddenFeatureError(error, resourceType)).to.be.true;
expect(warnMessage).to.equal('Forbidden resource access; - Skipping connections');
expect(warnMessage).to.equal('Forbidden resource access - Skipping connections');

// Restore original warn function
log.warn = originalWarn;
Expand All @@ -829,7 +829,44 @@ describe('#isForbiddenFeatureError', () => {
// eslint-disable-next-line no-unused-expressions
expect(utils.isForbiddenFeatureError(error, resourceType)).to.be.true;
expect(warnMessage).to.equal(
'Forbidden resource access;forbidden_resource_error - Skipping actions'
'Forbidden resource access (forbidden_resource_error) - Skipping actions'
);

// Restore original warn function
log.warn = originalWarn;
});

it('should use the clean message and errorCode from the response body when available', () => {
let warnMessage;
const originalWarn = log.warn;
// Mock the log.warn function
log.warn = (msg) => {
warnMessage = msg;
};

// Mirrors the real SDK ForbiddenError: top-level `message` is the full serialized body,
// while the human-readable message and errorCode live on originalError.response.body.
const error = {
message:
'ForbiddenError\nStatus code: 403\nBody: {\n "statusCode": 403,\n "errorCode": "legacy_mfa_phone_provider_not_allowed"\n}',
statusCode: 403,
originalError: {
response: {
body: {
statusCode: 403,
error: 'Forbidden',
message: 'Insufficient privileges to use this deprecated feature.',
errorCode: 'legacy_mfa_phone_provider_not_allowed',
},
},
},
};
const resourceType = 'guardianPhoneFactorSelectedProvider';

// eslint-disable-next-line no-unused-expressions
expect(utils.isForbiddenFeatureError(error, resourceType)).to.be.true;
expect(warnMessage).to.equal(
'Insufficient privileges to use this deprecated feature. (legacy_mfa_phone_provider_not_allowed) - Skipping guardianPhoneFactorSelectedProvider'
);

// Restore original warn function
Expand Down