mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-30 18:01:23 +08:00
feat(Google Cloud Storage Node): Allow custom OAuth2 scopes (#32659)
This commit is contained in:
@@ -115,6 +115,7 @@ export const GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE = [
|
||||
'wordpressOAuth2Api',
|
||||
'figmaOAuth2Api',
|
||||
'gumroadOAuth2Api',
|
||||
'googleCloudStorageOAuth2Api',
|
||||
];
|
||||
|
||||
export const ARTIFICIAL_TASK_DATA = {
|
||||
|
||||
@@ -1600,6 +1600,32 @@ describe('OauthService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not delete scope for googleCloudStorageOAuth2Api credentials', async () => {
|
||||
const credential = mock<CredentialsEntity>({
|
||||
id: '1',
|
||||
type: 'googleCloudStorageOAuth2Api',
|
||||
isManaged: false,
|
||||
});
|
||||
const mockDecryptedData = { clientId: 'client-id', scope: 'custom-scope' };
|
||||
const mockOAuthCredentials = { clientId: 'client-id', scope: 'custom-scope' };
|
||||
const mockAdditionalData = mock<IWorkflowExecuteAdditionalData>();
|
||||
|
||||
jest.mocked(WorkflowExecuteAdditionalData.getBase).mockResolvedValue(mockAdditionalData);
|
||||
credentialsHelper.getDecrypted.mockResolvedValue(mockDecryptedData);
|
||||
credentialsHelper.applyDefaultsAndOverwrites.mockResolvedValue(mockOAuthCredentials);
|
||||
|
||||
await service.getOAuthCredentials(credential);
|
||||
|
||||
expect(credentialsHelper.applyDefaultsAndOverwrites).toHaveBeenCalledWith(
|
||||
mockAdditionalData,
|
||||
{ clientId: 'client-id', scope: 'custom-scope' },
|
||||
credential.type,
|
||||
'internal',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not delete scope for non-OAuth2 credentials', async () => {
|
||||
const credential = mock<CredentialsEntity>({
|
||||
id: '1',
|
||||
|
||||
@@ -18,11 +18,42 @@ export class GoogleCloudStorageOAuth2Api implements ICredentialType {
|
||||
documentationUrl = 'google/oauth-single-service';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Custom Scopes',
|
||||
name: 'customScopes',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Define custom scopes',
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'The default scopes needed for the node to work are already set, If you change these the node may not function correctly.',
|
||||
name: 'customScopesNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
customScopes: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Enabled Scopes',
|
||||
name: 'enabledScopes',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
customScopes: [true],
|
||||
},
|
||||
},
|
||||
default: scopes.join(' '),
|
||||
description: 'Scopes that should be enabled',
|
||||
},
|
||||
{
|
||||
displayName: 'Scope',
|
||||
name: 'scope',
|
||||
type: 'hidden',
|
||||
default: scopes.join(' '),
|
||||
default: '={{$self["customScopes"] ? $self["enabledScopes"] : "' + scopes.join(' ') + '"}}',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { ClientOAuth2 } from '@n8n/client-oauth2';
|
||||
import nock from 'nock';
|
||||
|
||||
import { GoogleCloudStorageOAuth2Api } from '../GoogleCloudStorageOAuth2Api.credentials';
|
||||
|
||||
describe('GoogleCloudStorageOAuth2Api Credential', () => {
|
||||
const googleCloudStorageOAuth2Api = new GoogleCloudStorageOAuth2Api();
|
||||
const defaultScopes = [
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
'https://www.googleapis.com/auth/cloud-platform.read-only',
|
||||
'https://www.googleapis.com/auth/devstorage.full_control',
|
||||
'https://www.googleapis.com/auth/devstorage.read_only',
|
||||
'https://www.googleapis.com/auth/devstorage.read_write',
|
||||
];
|
||||
|
||||
// Shared OAuth2 configuration (inherited from googleOAuth2Api)
|
||||
const authorizationUri = 'https://accounts.google.com/o/oauth2/v2/auth';
|
||||
const tokenBaseUrl = 'https://oauth2.googleapis.com';
|
||||
const accessTokenUri = `${tokenBaseUrl}/token`;
|
||||
const redirectUri = 'http://localhost:5678/rest/oauth2-credential/callback';
|
||||
const clientId = 'test-client-id';
|
||||
const clientSecret = 'test-client-secret';
|
||||
|
||||
const createOAuthClient = (scopes: string[]) =>
|
||||
new ClientOAuth2({
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessTokenUri,
|
||||
authorizationUri,
|
||||
redirectUri,
|
||||
scopes,
|
||||
});
|
||||
|
||||
const mockTokenEndpoint = (code: string, responseScopes: string[]) => {
|
||||
nock(tokenBaseUrl)
|
||||
.post('/token', (body: Record<string, unknown>) => {
|
||||
return (
|
||||
body.code === code &&
|
||||
body.grant_type === 'authorization_code' &&
|
||||
body.redirect_uri === redirectUri
|
||||
);
|
||||
})
|
||||
.reply(200, {
|
||||
access_token: 'test-access-token',
|
||||
token_type: 'Bearer',
|
||||
expires_in: 3600,
|
||||
scope: responseScopes.join(' '),
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
nock.disableNetConnect();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
nock.restore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('should have correct credential metadata', () => {
|
||||
expect(googleCloudStorageOAuth2Api.name).toBe('googleCloudStorageOAuth2Api');
|
||||
expect(googleCloudStorageOAuth2Api.extends).toEqual(['googleOAuth2Api']);
|
||||
|
||||
// Custom scopes default to the node's required scopes
|
||||
const enabledScopesProperty = googleCloudStorageOAuth2Api.properties.find(
|
||||
(p) => p.name === 'enabledScopes',
|
||||
);
|
||||
expect(enabledScopesProperty?.default).toBe(defaultScopes.join(' '));
|
||||
});
|
||||
|
||||
it('should keep scope hidden and resolve it from the customScopes toggle', () => {
|
||||
const scopeProperty = googleCloudStorageOAuth2Api.properties.find((p) => p.name === 'scope');
|
||||
expect(scopeProperty?.type).toBe('hidden');
|
||||
expect(scopeProperty?.default).toContain('$self["customScopes"] ? $self["enabledScopes"]');
|
||||
expect(scopeProperty?.default).toContain(defaultScopes.join(' '));
|
||||
});
|
||||
|
||||
describe('OAuth2 flow with default scopes', () => {
|
||||
it('should include default scopes in authorization URI', () => {
|
||||
const oauthClient = createOAuthClient(defaultScopes);
|
||||
const authUri = oauthClient.code.getUri();
|
||||
|
||||
expect(authUri).toContain('scope=');
|
||||
expect(authUri).toContain('cloud-platform');
|
||||
expect(authUri).toContain('devstorage.full_control');
|
||||
expect(authUri).toContain(`client_id=${clientId}`);
|
||||
expect(authUri).toContain('response_type=code');
|
||||
});
|
||||
|
||||
it('should retrieve token successfully with default scopes', async () => {
|
||||
const code = 'test-auth-code';
|
||||
mockTokenEndpoint(code, defaultScopes);
|
||||
|
||||
const oauthClient = createOAuthClient(defaultScopes);
|
||||
const token = await oauthClient.code.getToken(redirectUri + `?code=${code}`);
|
||||
|
||||
expect(token.data.scope).toBe(defaultScopes.join(' '));
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth2 flow with custom scopes', () => {
|
||||
const customScopes = [...defaultScopes, 'https://www.googleapis.com/auth/compute'];
|
||||
|
||||
it('should include custom scopes in authorization URI', () => {
|
||||
const oauthClient = createOAuthClient(customScopes);
|
||||
const authUri = oauthClient.code.getUri();
|
||||
|
||||
expect(authUri).toContain('scope=');
|
||||
expect(authUri).toContain('cloud-platform');
|
||||
expect(authUri).toContain('compute');
|
||||
});
|
||||
|
||||
it('should handle completely different custom scopes', async () => {
|
||||
const differentScopes = ['https://www.googleapis.com/auth/devstorage.read_only'];
|
||||
const code = 'test-auth-code';
|
||||
mockTokenEndpoint(code, differentScopes);
|
||||
|
||||
const oauthClient = createOAuthClient(differentScopes);
|
||||
const authUri = oauthClient.code.getUri();
|
||||
|
||||
expect(authUri).toContain('devstorage.read_only');
|
||||
expect(authUri).not.toContain('cloud-platform');
|
||||
|
||||
const token = await oauthClient.code.getToken(redirectUri + `?code=${code}`);
|
||||
|
||||
expect(token.data.scope).toContain('devstorage.read_only');
|
||||
expect(token.data.scope).not.toContain('cloud-platform');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user