feat(core): Allow admin creation (#7837)

https://linear.app/n8n/issue/PAY-1038
This commit is contained in:
Iván Ovejero
2023-11-29 13:55:41 +01:00
committed by GitHub
parent 5ba5ed8e3c
commit 476806ebb0
9 changed files with 318 additions and 136 deletions

View File

@@ -281,6 +281,7 @@ export class Server extends AbstractServer {
activeWorkflowRunner,
Container.get(RoleService),
userService,
Container.get(License),
),
Container.get(SamlController),
Container.get(SourceControlController),
@@ -296,6 +297,7 @@ export class Server extends AbstractServer {
internalHooks,
externalHooks,
Container.get(UserService),
Container.get(License),
postHog,
),
Container.get(VariablesController),

View File

@@ -28,6 +28,7 @@ export class InvitationController {
private readonly internalHooks: IInternalHooksClass,
private readonly externalHooks: IExternalHooksClass,
private readonly userService: UserService,
private readonly license: License,
private readonly postHog?: PostHogClient,
) {}
@@ -88,11 +89,26 @@ export class InvitationController {
`Request to send email invite(s) to user(s) failed because of an invalid email address: ${invite.email}`,
);
}
if (invite.role && !['member', 'admin'].includes(invite.role)) {
throw new BadRequestError(
`Cannot invite user with invalid role: ${invite.role}. Please ensure all invitees' roles are either 'member' or 'admin'.`,
);
}
if (invite.role === 'admin' && !this.license.isAdvancedPermissionsLicensed()) {
throw new UnauthorizedError(
'Cannot invite admin user without advanced permissions. Please upgrade to a license that includes this feature.',
);
}
});
const emails = req.body.map((e) => e.email);
const attributes = req.body.map(({ email, role }) => ({
email,
role: role ?? 'member',
}));
const { usersInvited, usersCreated } = await this.userService.inviteMembers(req.user, emails);
const { usersInvited, usersCreated } = await this.userService.inviteUsers(req.user, attributes);
await this.externalHooks.run('user.invited', [usersCreated]);

View File

@@ -19,6 +19,7 @@ import { Logger } from '@/Logger';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { License } from '@/License';
@Authorized()
@RestController('/users')
@@ -32,6 +33,7 @@ export class UsersController {
private readonly activeWorkflowRunner: ActiveWorkflowRunner,
private readonly roleService: RoleService,
private readonly userService: UserService,
private readonly license: License,
) {}
static ERROR_MESSAGES = {
@@ -43,6 +45,7 @@ export class UsersController {
NO_ADMIN_ON_OWNER: 'Admin cannot change role on global owner',
NO_OWNER_ON_OWNER: 'Owner cannot change role on global owner',
NO_USER_TO_OWNER: 'Cannot promote user to global owner',
NO_ADMIN_IF_UNLICENSED: 'Admin role is not available without a license',
},
} as const;
@@ -336,6 +339,7 @@ export class UsersController {
NO_USER_TO_OWNER,
NO_USER,
NO_OWNER_ON_OWNER,
NO_ADMIN_IF_UNLICENSED,
} = UsersController.ERROR_MESSAGES.CHANGE_ROLE;
if (req.user.globalRole.scope === 'global' && req.user.globalRole.name === 'member') {
@@ -364,6 +368,14 @@ export class UsersController {
throw new NotFoundError(NO_USER);
}
if (
newRole.scope === 'global' &&
newRole.name === 'admin' &&
!this.license.isAdvancedPermissionsLicensed()
) {
throw new UnauthorizedError(NO_ADMIN_IF_UNLICENSED);
}
if (
req.user.globalRole.scope === 'global' &&
req.user.globalRole.name === 'admin' &&

View File

@@ -296,7 +296,11 @@ export declare namespace PasswordResetRequest {
// ----------------------------------
export declare namespace UserRequest {
export type Invite = AuthenticatedRequest<{}, {}, Array<{ email: string }>>;
export type Invite = AuthenticatedRequest<
{},
{},
Array<{ email: string; role?: 'member' | 'admin' }>
>;
export type InviteResponse = {
user: { id: string; email: string; inviteAcceptUrl?: string; emailSent: boolean };

View File

@@ -238,18 +238,19 @@ export class UserService {
);
}
public async inviteMembers(owner: User, emails: string[]) {
async inviteUsers(owner: User, attributes: Array<{ email: string; role: 'member' | 'admin' }>) {
const memberRole = await this.roleService.findGlobalMemberRole();
const adminRole = await this.roleService.findGlobalAdminRole();
const existingUsers = await this.findMany({
where: { email: In(emails) },
where: { email: In(attributes.map(({ email }) => email)) },
relations: ['globalRole'],
select: ['email', 'password', 'id'],
});
const existUsersEmails = existingUsers.map((user) => user.email);
const toCreateUsers = emails.filter((email) => !existUsersEmails.includes(email));
const toCreateUsers = attributes.filter(({ email }) => !existUsersEmails.includes(email));
const pendingUsersToInvite = existingUsers.filter((email) => email.isPending);
@@ -264,10 +265,10 @@ export class UserService {
try {
await this.getManager().transaction(async (transactionManager) =>
Promise.all(
toCreateUsers.map(async (email) => {
toCreateUsers.map(async ({ email, role }) => {
const newUser = Object.assign(new User(), {
email,
globalRole: memberRole,
globalRole: role === 'member' ? memberRole : adminRole,
});
const savedUser = await transactionManager.save<User>(newUser);
createdUsers.set(email, savedUser.id);
@@ -285,6 +286,6 @@ export class UserService {
const usersInvited = await this.sendEmails(owner, Object.fromEntries(createdUsers));
return { usersInvited, usersCreated: toCreateUsers };
return { usersInvited, usersCreated: toCreateUsers.map(({ email }) => email) };
}
}