mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-29 01:39:24 +08:00
feat: Show scopes for managed OAuth credentials with N8N_MANAGED_OAUTH_SHOW_SCOPES (backport to release-candidate/2.35.x) (#36397)
Co-authored-by: yehorkardash <yehor.kardash@n8n.io> Co-authored-by: Jon <jonathan.bennetts@gmail.com>
This commit is contained in:
committed by
GitHub
parent
0dccd14a44
commit
d8e3ee6c7e
@@ -30,6 +30,14 @@ class CredentialsOverwrite {
|
||||
*/
|
||||
@Env('N8N_SKIP_CREDENTIAL_OVERWRITE')
|
||||
skipTypes: CommaSeparatedStringArray<string> = [];
|
||||
|
||||
/**
|
||||
* Comma-separated list of managed OAuth credential types whose scope fields
|
||||
* remain user-configurable.
|
||||
* @example `N8N_MANAGED_OAUTH_SHOW_SCOPES=googleOAuth2Api`
|
||||
*/
|
||||
@Env('N8N_MANAGED_OAUTH_SHOW_SCOPES')
|
||||
showScopes: CommaSeparatedStringArray<string> = [];
|
||||
}
|
||||
|
||||
@Config
|
||||
|
||||
@@ -135,6 +135,7 @@ describe('GlobalConfig', () => {
|
||||
endpoint: '',
|
||||
endpointAuthToken: '',
|
||||
persistence: false,
|
||||
showScopes: [],
|
||||
skipTypes: [],
|
||||
},
|
||||
},
|
||||
@@ -742,6 +743,18 @@ describe('GlobalConfig', () => {
|
||||
expect(config.agents.sandboxSnapshot).toBe('n8n/agent-knowledge:1.2.3');
|
||||
});
|
||||
|
||||
it('should parse N8N_MANAGED_OAUTH_SHOW_SCOPES from env variables', () => {
|
||||
process.env = {
|
||||
N8N_MANAGED_OAUTH_SHOW_SCOPES: 'googleOAuth2Api,microsoftOAuth2Api',
|
||||
};
|
||||
const config = Container.get(GlobalConfig);
|
||||
|
||||
expect(config.credentials.overwrite.showScopes).toEqual([
|
||||
'googleOAuth2Api',
|
||||
'microsoftOAuth2Api',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use values from env variables when defined', () => {
|
||||
process.env = {
|
||||
DB_POSTGRESDB_HOST: 'some-host',
|
||||
|
||||
@@ -838,10 +838,54 @@ describe('FrontendService', () => {
|
||||
describe('overwriteCredentialsProperties', () => {
|
||||
afterEach(() => {
|
||||
// Restore globalConfig.credentials to the default so other tests are unaffected
|
||||
(globalConfig as any).credentials = { overwrite: { skipTypes: [] } };
|
||||
(globalConfig as any).credentials = { overwrite: { showScopes: [], skipTypes: [] } };
|
||||
loadNodesAndCredentials.types = { credentials: [], nodes: [] };
|
||||
});
|
||||
|
||||
it('should expose managed OAuth scopes only for configured credential types', () => {
|
||||
const baseCredential = {
|
||||
name: 'googleOAuth2Api',
|
||||
displayName: 'Google OAuth2 API',
|
||||
properties: [],
|
||||
} as ICredentialType;
|
||||
const childCredential = {
|
||||
name: 'googleSheetsOAuth2Api',
|
||||
displayName: 'Google Sheets OAuth2 API',
|
||||
properties: [],
|
||||
} as ICredentialType;
|
||||
|
||||
loadNodesAndCredentials.types = {
|
||||
credentials: [baseCredential, childCredential],
|
||||
nodes: [],
|
||||
};
|
||||
(globalConfig as any).credentials = {
|
||||
overwrite: { showScopes: ['googleOAuth2Api'], skipTypes: [] },
|
||||
};
|
||||
|
||||
const { service } = createMockService();
|
||||
(service as any).overwriteCredentialsProperties();
|
||||
|
||||
expect(baseCredential.__showManagedOAuthScopes).toBe(true);
|
||||
expect(childCredential.__showManagedOAuthScopes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should clear stale managed OAuth scope visibility metadata', () => {
|
||||
const credential = {
|
||||
name: 'googleOAuth2Api',
|
||||
displayName: 'Google OAuth2 API',
|
||||
properties: [],
|
||||
__showManagedOAuthScopes: true,
|
||||
} as ICredentialType;
|
||||
|
||||
loadNodesAndCredentials.types = { credentials: [credential], nodes: [] };
|
||||
(globalConfig as any).credentials = { overwrite: { showScopes: [], skipTypes: [] } };
|
||||
|
||||
const { service } = createMockService();
|
||||
(service as any).overwriteCredentialsProperties();
|
||||
|
||||
expect(credential.__showManagedOAuthScopes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set __skipManagedCreation for types in the skip list', () => {
|
||||
const skipCredential = {
|
||||
name: 'googleSheetsOAuth2Api',
|
||||
|
||||
@@ -764,11 +764,12 @@ export class FrontendService {
|
||||
private overwriteCredentialsProperties() {
|
||||
const { credentials } = this.loadNodesAndCredentials.types;
|
||||
const credentialsOverwrites = this.credentialsOverwrites.getAll();
|
||||
const { skipTypes } = this.globalConfig.credentials.overwrite;
|
||||
const { showScopes = [], skipTypes } = this.globalConfig.credentials.overwrite;
|
||||
for (const credential of credentials) {
|
||||
// Clear any existing overwritten properties to prevent stale data
|
||||
delete credential.__overwrittenProperties;
|
||||
delete credential.__skipManagedCreation;
|
||||
delete credential.__showManagedOAuthScopes;
|
||||
|
||||
const overwrittenProperties = [];
|
||||
this.credentialTypes
|
||||
@@ -793,6 +794,10 @@ export class FrontendService {
|
||||
credential.__skipManagedCreation = true;
|
||||
}
|
||||
|
||||
if (showScopes.includes(credential.name)) {
|
||||
credential.__showManagedOAuthScopes = true;
|
||||
}
|
||||
|
||||
// Inject the per-instance JWKS URI as the default of any `jwksUri`
|
||||
// property on `oAuth2Api` itself or any credential that extends it
|
||||
// and explicitly re-declares the field. Inheritance is blocked by
|
||||
|
||||
+24
@@ -783,6 +783,30 @@ describe('CredentialEdit', () => {
|
||||
expect(queryByText('Custom Scopes Notice')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows scope fields when enabled for the managed OAuth credential type', async () => {
|
||||
const credentialsStore = setupManagedCapableStores({
|
||||
grantType: 'authorizationCode',
|
||||
customScopes: true,
|
||||
});
|
||||
credentialsStore.state.credentialTypes[discordOAuth2ApiManagedCapable.name] = {
|
||||
...discordOAuth2ApiManagedCapable,
|
||||
__showManagedOAuthScopes: true,
|
||||
};
|
||||
|
||||
const { queryByText } = renderComponent({
|
||||
props: {
|
||||
activeId: 'cred-2',
|
||||
modalName: CREDENTIAL_EDIT_MODAL_KEY,
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
await retry(() => expect(credentialsStore.getCredentialData).toHaveBeenCalled());
|
||||
|
||||
expect(queryByText('Custom Scopes')).toBeInTheDocument();
|
||||
expect(queryByText('Enabled Scopes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows scope fields when managed OAuth is available but user has provided their own clientId/clientSecret', async () => {
|
||||
const credentialsStore = setupManagedCapableStores({
|
||||
grantType: 'authorizationCode',
|
||||
|
||||
+3
-2
@@ -357,10 +357,11 @@ export function useCredentialForm(options: UseCredentialFormOptions) {
|
||||
|
||||
function displayCredentialParameter(parameter: INodeProperties): boolean {
|
||||
if (parameter.type === 'hidden') return false;
|
||||
|
||||
const isManagedCredential = isEditingManagedCredential.value || isManagedOAuthMode.value;
|
||||
if (
|
||||
MANAGED_CREDENTIAL_HIDDEN_PROPERTIES.has(parameter.name) &&
|
||||
(isEditingManagedCredential.value || isManagedOAuthMode.value)
|
||||
isManagedCredential &&
|
||||
!credentialType.value?.__showManagedOAuthScopes
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@ export interface ICredentialType {
|
||||
documentationUrl?: string;
|
||||
__overwrittenProperties?: string[];
|
||||
__skipManagedCreation?: boolean;
|
||||
__showManagedOAuthScopes?: boolean;
|
||||
authenticate?: IAuthenticate;
|
||||
preAuthentication?: (
|
||||
this: IHttpRequestHelper,
|
||||
|
||||
Reference in New Issue
Block a user