UNPKG

n8n

Version:

n8n Workflow Automation Tool

196 lines 9.47 kB
"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