Skip to content

Commit

Permalink
feat(core): Allow admin creation (#7837)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivov authored Nov 29, 2023
1 parent 5ba5ed8 commit 476806e
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 131 deletions.
2 changes: 2 additions & 0 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export class Server extends AbstractServer {
activeWorkflowRunner,
Container.get(RoleService),
userService,
Container.get(License),
),
Container.get(SamlController),
Container.get(SourceControlController),
Expand All @@ -296,6 +297,7 @@ export class Server extends AbstractServer {
internalHooks,
externalHooks,
Container.get(UserService),
Container.get(License),
postHog,
),
Container.get(VariablesController),
Expand Down
20 changes: 18 additions & 2 deletions packages/cli/src/controllers/invitation.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {}

Expand Down Expand Up @@ -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]);

Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/controllers/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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 = {
Expand All @@ -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;

Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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' &&
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
13 changes: 7 additions & 6 deletions packages/cli/src/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
Expand All @@ -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) };
}
}
Loading

0 comments on commit 476806e

Please sign in to comment.