n8n
Version:
n8n Workflow Automation Tool
196 lines • 9.47 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.IdentityResolutionService = void 0;
const backend_common_1 = require("@n8n/backend-common");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const event_service_1 = require("../../../events/event.service");
const user_service_1 = require("../../../services/user.service");
const token_exchange_errors_1 = require("../token-exchange.errors");
const token_exchange_types_1 = require("../token-exchange.types");
const INVALID_PASSWORD_PLACEHOLDER = '!token-exchange-no-password';
const MAX_NAME_LENGTH = 32;
function isGlobalRole(role) {
return role in db_1.GLOBAL_ROLES;
}
function trimName(value, fallback = '') {
return (value ?? fallback).slice(0, MAX_NAME_LENGTH);
}
let IdentityResolutionService = class IdentityResolutionService {
constructor(logger, userRepository, authIdentityRepository, eventService, userService) {
this.userRepository = userRepository;
this.authIdentityRepository = authIdentityRepository;
this.eventService = eventService;
this.userService = userService;
this.logger = logger.scoped('token-exchange');
}
async resolve(claims, allowedRoles, tokenContext) {
const email = claims.email?.toLowerCase();
const identity = await this.authIdentityRepository.findOne({
where: { providerId: claims.sub, providerType: 'token-exchange' },
relations: { user: { role: true } },
});
if (identity) {
return await this.resolveByIdentity(claims, identity, allowedRoles, tokenContext);
}
if (email) {
const existingUser = await this.userRepository.findOne({
where: { email },
relations: ['authIdentities', 'role'],
});
if (existingUser) {
return await this.resolveByEmail(claims, email, existingUser, allowedRoles, tokenContext);
}
}
if (!email) {
throw new token_exchange_errors_1.TokenExchangeAuthError(token_exchange_types_1.TokenExchangeFailureReason.InvalidClaims, 'Email claim is required for user provisioning');
}
return await this.provisionUser(claims, email, allowedRoles, tokenContext);
}
async resolveByIdentity(claims, identity, allowedRoles, tokenContext) {
this.logger.debug('Resolved user by auth identity', { sub: claims.sub });
const resolvedRole = this.resolveRoleForExistingUser(claims.role, allowedRoles, identity.user.role?.slug);
return await this.syncProfile(identity.user, claims, resolvedRole, tokenContext);
}
async resolveByEmail(claims, email, existingUser, allowedRoles, tokenContext) {
this.logger.debug('Linking external identity to existing user by email', {
sub: claims.sub,
email,
});
const resolvedRole = this.resolveRoleForExistingUser(claims.role, allowedRoles, existingUser.role?.slug);
await this.authIdentityRepository.save(db_1.AuthIdentity.create(existingUser, claims.sub, 'token-exchange'));
this.eventService.emit('token-exchange-identity-linked', {
userId: existingUser.id,
sub: claims.sub,
email,
kid: tokenContext?.kid ?? '',
issuer: tokenContext?.issuer ?? claims.iss,
});
return await this.syncProfile(existingUser, claims, resolvedRole, tokenContext);
}
async provisionUser(claims, email, allowedRoles, tokenContext) {
this.logger.debug('JIT provisioning new user', { sub: claims.sub, email });
const jitRole = this.resolveRoleForNewUser(claims.role, allowedRoles);
const targetRole = jitRole ? { slug: jitRole } : db_1.GLOBAL_MEMBER_ROLE;
const user = await this.userRepository.manager.transaction(async (trx) => {
const { user: newUser } = await this.userRepository.createUserWithProject({
email,
firstName: trimName(claims.given_name),
lastName: trimName(claims.family_name),
role: targetRole,
password: INVALID_PASSWORD_PLACEHOLDER,
}, trx);
await trx.save(trx.create(db_1.AuthIdentity, {
providerId: claims.sub,
providerType: 'token-exchange',
userId: newUser.id,
}));
return newUser;
});
this.eventService.emit('token-exchange-user-provisioned', {
userId: user.id,
sub: claims.sub,
email,
role: targetRole.slug,
kid: tokenContext?.kid ?? '',
issuer: tokenContext?.issuer ?? claims.iss,
});
return user;
}
resolveRoleForExistingUser(roleClaim, allowedRoles, currentRole) {
if (roleClaim === undefined)
return undefined;
if (currentRole === 'global:owner') {
this.logger.debug('Skipping role sync for existing owner');
return undefined;
}
const role = roleClaim;
if (role === 'global:owner') {
this.logger.warn('Ignoring global:owner role claim for existing user');
return undefined;
}
if (!isGlobalRole(role)) {
this.logger.warn('Unknown role claim ignored', { role });
return undefined;
}
if (allowedRoles && allowedRoles.length > 0 && !allowedRoles.includes(role)) {
throw new token_exchange_errors_1.TokenExchangeAuthError(token_exchange_types_1.TokenExchangeFailureReason.RoleNotAllowed, `Role '${role}' is not allowed for this token exchange key`);
}
return role;
}
resolveRoleForNewUser(roleClaim, allowedRoles) {
if (roleClaim === undefined)
return undefined;
const role = roleClaim;
if (role === 'global:owner') {
throw new token_exchange_errors_1.TokenExchangeAuthError(token_exchange_types_1.TokenExchangeFailureReason.RoleNotAllowed, 'Cannot provision global:owner role via token exchange');
}
if (!isGlobalRole(role)) {
throw new token_exchange_errors_1.TokenExchangeAuthError(token_exchange_types_1.TokenExchangeFailureReason.RoleNotAllowed, `Unrecognized role '${role}' cannot be assigned to new user`);
}
if (allowedRoles && allowedRoles.length > 0 && !allowedRoles.includes(role)) {
throw new token_exchange_errors_1.TokenExchangeAuthError(token_exchange_types_1.TokenExchangeFailureReason.RoleNotAllowed, `Role '${role}' is not allowed for this token exchange key`);
}
return role;
}
async syncProfile(user, claims, resolvedRole, tokenContext) {
let needsReload = false;
const profileUpdates = {};
if (claims.given_name !== undefined) {
const trimmed = trimName(claims.given_name);
if (trimmed !== user.firstName) {
profileUpdates.firstName = trimmed;
}
}
if (claims.family_name !== undefined) {
const trimmed = trimName(claims.family_name);
if (trimmed !== user.lastName) {
profileUpdates.lastName = trimmed;
}
}
if (Object.keys(profileUpdates).length > 0) {
await this.userRepository.update(user.id, profileUpdates);
needsReload = true;
}
const previousRole = user.role?.slug;
if (resolvedRole && resolvedRole !== previousRole) {
await this.userService.changeUserRole(user, { newRoleName: resolvedRole });
needsReload = true;
if (previousRole) {
this.eventService.emit('token-exchange-role-updated', {
userId: user.id,
previousRole,
newRole: resolvedRole,
kid: tokenContext?.kid ?? '',
issuer: tokenContext?.issuer ?? claims.iss,
});
}
}
if (needsReload) {
return await this.userRepository.findOneOrFail({
where: { id: user.id },
relations: ['role'],
});
}
return user;
}
};
exports.IdentityResolutionService = IdentityResolutionService;
exports.IdentityResolutionService = IdentityResolutionService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [backend_common_1.Logger,
db_1.UserRepository,
db_1.AuthIdentityRepository,
event_service_1.EventService,
user_service_1.UserService])
], IdentityResolutionService);
//# sourceMappingURL=identity-resolution.service.js.map