mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
feat: Allow OIDC logins to sync instance roles (#21055)
Co-authored-by: konstantintieber <konstantin.tieber@n8n.io>
This commit is contained in:
co-authored by
konstantintieber
parent
83d8b7e720
commit
fcbf1433f6
@@ -501,7 +501,7 @@ describe('ProvisioningService', () => {
|
||||
expect(config).toEqual({ ...provisioningConfigDto, scopesProvisionInstanceRole: false });
|
||||
expect(provisioningService.loadConfig).toHaveBeenCalledTimes(1);
|
||||
expect(provisioningService.getConfig).toHaveBeenCalledTimes(2);
|
||||
expect(settingsRepository.update).toHaveBeenCalledTimes(1);
|
||||
expect(settingsRepository.upsert).toHaveBeenCalledTimes(1);
|
||||
expect(publisher.publishCommand).toHaveBeenCalledTimes(1);
|
||||
expect(publisher.publishCommand).toHaveBeenCalledWith({
|
||||
command: 'reload-sso-provisioning-configuration',
|
||||
|
||||
@@ -254,13 +254,18 @@ export class ProvisioningService {
|
||||
|
||||
ProvisioningConfigDto.parse(updatedConfig);
|
||||
|
||||
await this.settingsRepository.update(
|
||||
{ key: PROVISIONING_PREFERENCES_DB_KEY },
|
||||
{ value: JSON.stringify(updatedConfig) },
|
||||
await this.settingsRepository.upsert(
|
||||
{
|
||||
key: PROVISIONING_PREFERENCES_DB_KEY,
|
||||
value: JSON.stringify(updatedConfig),
|
||||
loadOnStartup: true,
|
||||
},
|
||||
{ conflictPaths: ['key'] },
|
||||
);
|
||||
|
||||
await this.publisher.publishCommand({ command: 'reload-sso-provisioning-configuration' });
|
||||
this.provisioningConfig = await this.loadConfig();
|
||||
|
||||
await this.publisher.publishCommand({ command: 'reload-sso-provisioning-configuration' });
|
||||
return await this.getConfig();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { OIDC_PREFERENCES_DB_KEY } from '../constants';
|
||||
import { OidcService } from '../oidc.service.ee';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import type { OidcConfigDto } from '@n8n/api-types';
|
||||
import { type ProvisioningService } from '@/modules/provisioning.ee/provisioning.service.ee';
|
||||
|
||||
describe('OidcService', () => {
|
||||
let oidcService: OidcService;
|
||||
@@ -25,6 +26,7 @@ describe('OidcService', () => {
|
||||
let cipher: Cipher;
|
||||
let logger: Logger;
|
||||
let jwtService: JwtService;
|
||||
let provisioningService: ProvisioningService;
|
||||
|
||||
const mockOidcConfig = {
|
||||
clientId: 'test-client-id',
|
||||
@@ -56,6 +58,7 @@ describe('OidcService', () => {
|
||||
cipher = mock<Cipher>();
|
||||
logger = mockLogger();
|
||||
jwtService = mock<JwtService>();
|
||||
provisioningService = mock<ProvisioningService>();
|
||||
|
||||
jest
|
||||
.spyOn(ssoHelpers, 'setCurrentAuthenticationMethod')
|
||||
@@ -71,6 +74,7 @@ describe('OidcService', () => {
|
||||
logger,
|
||||
jwtService,
|
||||
instanceSettings,
|
||||
provisioningService,
|
||||
);
|
||||
|
||||
await oidcService.init();
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from '../sso-helpers';
|
||||
import { OIDC_CLIENT_SECRET_REDACTED_VALUE, OIDC_PREFERENCES_DB_KEY } from './constants';
|
||||
import { OnPubSubEvent } from '@n8n/decorators';
|
||||
import { ProvisioningService } from '@/modules/provisioning.ee/provisioning.service.ee';
|
||||
|
||||
const DEFAULT_OIDC_CONFIG: OidcConfigDto = {
|
||||
clientId: '',
|
||||
@@ -66,6 +67,7 @@ export class OidcService {
|
||||
private readonly logger: Logger,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly instanceSettings: InstanceSettings,
|
||||
private readonly provisioningService: ProvisioningService,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
@@ -176,13 +178,14 @@ export class OidcService {
|
||||
|
||||
const prompt = this.oidcConfig.prompt;
|
||||
|
||||
const provisioning = this.globalConfig.sso.provisioning;
|
||||
const provisioningConfig = await this.provisioningService.getConfig();
|
||||
const provisioningEnabled =
|
||||
provisioning.scopesProvisionInstanceRole || provisioning.scopesProvisionProjectRoles;
|
||||
provisioningConfig.scopesProvisionInstanceRole ||
|
||||
provisioningConfig.scopesProvisionProjectRoles;
|
||||
|
||||
// Include the custom n8n scope if provisioning is enabled
|
||||
const scope = provisioningEnabled
|
||||
? `openid email profile ${provisioning.scopesName}`
|
||||
? `openid email profile ${provisioningConfig.scopesName}`
|
||||
: 'openid email profile';
|
||||
|
||||
const authorizationURL = client.buildAuthorizationUrl(configuration, {
|
||||
@@ -242,6 +245,11 @@ export class OidcService {
|
||||
throw new BadRequestError('Invalid email format');
|
||||
}
|
||||
|
||||
const provisioningConfig = await this.provisioningService.getConfig();
|
||||
const provisioningEnabled =
|
||||
provisioningConfig.scopesProvisionInstanceRole ||
|
||||
provisioningConfig.scopesProvisionProjectRoles;
|
||||
|
||||
const openidUser = await this.authIdentityRepository.findOne({
|
||||
where: { providerId: claims.sub, providerType: 'oidc' },
|
||||
relations: {
|
||||
@@ -252,6 +260,13 @@ export class OidcService {
|
||||
});
|
||||
|
||||
if (openidUser) {
|
||||
if (provisioningEnabled) {
|
||||
await this.provisioningService.provisionInstanceRoleForUser(
|
||||
openidUser.user,
|
||||
claims.n8n_instance_role,
|
||||
);
|
||||
}
|
||||
|
||||
return openidUser.user;
|
||||
}
|
||||
|
||||
@@ -297,6 +312,10 @@ export class OidcService {
|
||||
}),
|
||||
);
|
||||
|
||||
if (provisioningEnabled) {
|
||||
await this.provisioningService.provisionInstanceRoleForUser(user, claims.n8n_instance_role);
|
||||
}
|
||||
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { createUser } from '@test-integration/db/users';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import { JwtService } from '@/services/jwt.service';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { ProvisioningService } from '@/modules/provisioning.ee/provisioning.service.ee';
|
||||
|
||||
beforeAll(async () => {
|
||||
await testDb.init();
|
||||
@@ -334,8 +335,12 @@ describe('OIDC service', () => {
|
||||
};
|
||||
|
||||
it('should not include the provisioning scope if no provisioning is enabled', async () => {
|
||||
Container.get(GlobalConfig).sso.provisioning.scopesProvisionProjectRoles = false;
|
||||
Container.get(GlobalConfig).sso.provisioning.scopesProvisionInstanceRole = false;
|
||||
// @ts-expect-error - provisioningConfig is private and only accessible within the class
|
||||
Container.get(ProvisioningService).provisioningConfig.scopesProvisionProjectRoles = false;
|
||||
// @ts-expect-error - provisioningConfig is private and only accessible within the class
|
||||
Container.get(ProvisioningService).provisioningConfig.scopesProvisionInstanceRole = false;
|
||||
// @ts-expect-error - provisioningConfig is private and only accessible within the class
|
||||
Container.get(ProvisioningService).provisioningConfig.scopesName = 'n8n_test_scope';
|
||||
const authUrl = await oidcService.generateLoginUrl();
|
||||
|
||||
validateUrl(authUrl);
|
||||
@@ -343,9 +348,12 @@ describe('OIDC service', () => {
|
||||
});
|
||||
|
||||
it('should include the provisioning scope if project provisioning is enabled', async () => {
|
||||
Container.get(GlobalConfig).sso.provisioning.scopesProvisionProjectRoles = true;
|
||||
Container.get(GlobalConfig).sso.provisioning.scopesProvisionInstanceRole = false;
|
||||
Container.get(GlobalConfig).sso.provisioning.scopesName = 'n8n_test_scope';
|
||||
// @ts-expect-error - provisioningConfig is private and only accessible within the class
|
||||
Container.get(ProvisioningService).provisioningConfig.scopesProvisionProjectRoles = true;
|
||||
// @ts-expect-error - provisioningConfig is private and only accessible within the class
|
||||
Container.get(ProvisioningService).provisioningConfig.scopesProvisionInstanceRole = false;
|
||||
// @ts-expect-error - provisioningConfig is private and only accessible within the class
|
||||
Container.get(ProvisioningService).provisioningConfig.scopesName = 'n8n_test_scope';
|
||||
const authUrl = await oidcService.generateLoginUrl();
|
||||
|
||||
validateUrl(authUrl);
|
||||
@@ -353,9 +361,12 @@ describe('OIDC service', () => {
|
||||
});
|
||||
|
||||
it('should include the provisioning scope if instance provisioning is enabled', async () => {
|
||||
Container.get(GlobalConfig).sso.provisioning.scopesProvisionProjectRoles = false;
|
||||
Container.get(GlobalConfig).sso.provisioning.scopesProvisionInstanceRole = true;
|
||||
Container.get(GlobalConfig).sso.provisioning.scopesName = 'n8n_test_scope';
|
||||
// @ts-expect-error - provisioningConfig is private and only accessible within the class
|
||||
Container.get(ProvisioningService).provisioningConfig.scopesProvisionProjectRoles = false;
|
||||
// @ts-expect-error - provisioningConfig is private and only accessible within the class
|
||||
Container.get(ProvisioningService).provisioningConfig.scopesProvisionInstanceRole = true;
|
||||
// @ts-expect-error - provisioningConfig is private and only accessible within the class
|
||||
Container.get(ProvisioningService).provisioningConfig.scopesName = 'n8n_test_scope';
|
||||
const authUrl = await oidcService.generateLoginUrl();
|
||||
|
||||
validateUrl(authUrl);
|
||||
@@ -363,9 +374,12 @@ describe('OIDC service', () => {
|
||||
});
|
||||
|
||||
it('should include the provisioning scope if project and instance provisioning is enabled', async () => {
|
||||
Container.get(GlobalConfig).sso.provisioning.scopesProvisionProjectRoles = true;
|
||||
Container.get(GlobalConfig).sso.provisioning.scopesProvisionInstanceRole = true;
|
||||
Container.get(GlobalConfig).sso.provisioning.scopesName = 'n8n_test_scope';
|
||||
// @ts-expect-error - provisioningConfig is private and only accessible within the class
|
||||
Container.get(ProvisioningService).provisioningConfig.scopesProvisionProjectRoles = true;
|
||||
// @ts-expect-error - provisioningConfig is private and only accessible within the class
|
||||
Container.get(ProvisioningService).provisioningConfig.scopesProvisionInstanceRole = true;
|
||||
// @ts-expect-error - provisioningConfig is private and only accessible within the class
|
||||
Container.get(ProvisioningService).provisioningConfig.scopesName = 'n8n_test_scope';
|
||||
const authUrl = await oidcService.generateLoginUrl();
|
||||
|
||||
validateUrl(authUrl);
|
||||
|
||||
Reference in New Issue
Block a user