feat(core): Support create, delete, edit role for users in Public API (#10279)

This commit is contained in:
Iván Ovejero 2024-08-02 12:06:17 +02:00 committed by GitHub
parent a533916628
commit 84efbd9b9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 386 additions and 0 deletions

View file

@ -0,0 +1,31 @@
patch:
x-eov-operation-id: changeRole
x-eov-operation-handler: v1/handlers/users/users.handler.ee
tags:
- User
summary: Change a user's global role
description: Change a user's global role
parameters:
- $ref: '../schemas/parameters/userIdentifier.yml'
requestBody:
description: New role for the user
required: true
content:
application/json:
schema:
type: object
properties:
newRoleName:
type: string
enum: [global:admin, global:member]
required:
- newRoleName
responses:
'200':
description: Operation successful.
'401':
$ref: '../../../../shared/spec/responses/unauthorized.yml'
'403':
$ref: '../../../../shared/spec/responses/forbidden.yml'
'404':
$ref: '../../../../shared/spec/responses/notFound.yml'

View file

@ -17,3 +17,21 @@ get:
$ref: '../schemas/user.yml'
'401':
$ref: '../../../../shared/spec/responses/unauthorized.yml'
delete:
x-eov-operation-id: deleteUser
x-eov-operation-handler: v1/handlers/users/users.handler.ee
tags:
- User
summary: Delete a user
description: Delete a user from your instance.
parameters:
- $ref: '../schemas/parameters/userIdentifier.yml'
responses:
'204':
description: Operation successful.
'401':
$ref: '../../../../shared/spec/responses/unauthorized.yml'
'403':
$ref: '../../../../shared/spec/responses/forbidden.yml'
'404':
$ref: '../../../../shared/spec/responses/notFound.yml'

View file

@ -18,3 +18,53 @@ get:
$ref: '../schemas/userList.yml'
'401':
$ref: '../../../../shared/spec/responses/unauthorized.yml'
post:
x-eov-operation-id: createUser
x-eov-operation-handler: v1/handlers/users/users.handler.ee
tags:
- User
summary: Create multiple users
description: Create one or more users.
requestBody:
description: Array of users to be created.
required: true
content:
application/json:
schema:
type: array
items:
type: object
properties:
email:
type: string
format: email
role:
type: string
enum: [global:admin, global:member]
required:
- email
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
type: object
properties:
user:
type: object
properties:
id:
type: string
email:
type: string
inviteAcceptUrl:
type: string
emailSent:
type: boolean
error:
type: string
'401':
$ref: '../../../../shared/spec/responses/unauthorized.yml'
'403':
$ref: '../../../../shared/spec/responses/forbidden.yml'

View file

@ -6,11 +6,19 @@ import { clean, getAllUsersAndCount, getUser } from './users.service.ee';
import { encodeNextCursor } from '../../shared/services/pagination.service';
import {
globalScope,
isLicensed,
validCursor,
validLicenseWithUserQuota,
} from '../../shared/middlewares/global.middleware';
import type { UserRequest } from '@/requests';
import { InternalHooks } from '@/InternalHooks';
import type { Response } from 'express';
import { InvitationController } from '@/controllers/invitation.controller';
import { UsersController } from '@/controllers/users.controller';
type Create = UserRequest.Invite;
type Delete = UserRequest.Delete;
type ChangeRole = UserRequest.ChangeRole;
export = {
getUser: [
@ -68,4 +76,29 @@ export = {
});
},
],
createUser: [
globalScope('user:create'),
async (req: Create, res: Response) => {
const usersInvited = await Container.get(InvitationController).inviteUser(req);
return res.status(201).json(usersInvited);
},
],
deleteUser: [
globalScope('user:delete'),
async (req: Delete, res: Response) => {
await Container.get(UsersController).deleteUser(req);
return res.status(204).send();
},
],
changeRole: [
isLicensed('feat:advancedPermissions'),
globalScope('user:changeRole'),
async (req: ChangeRole, res: Response) => {
await Container.get(UsersController).changeGlobalRole(req);
return res.status(204).send();
},
],
};

View file

@ -70,6 +70,8 @@ paths:
$ref: './handlers/users/spec/paths/users.yml'
/users/{id}:
$ref: './handlers/users/spec/paths/users.id.yml'
/users/{id}/role:
$ref: './handlers/users/spec/paths/users.id.role.yml'
/source-control/pull:
$ref: './handlers/sourceControl/spec/paths/sourceControl.yml'
/variables:

View file

@ -0,0 +1,252 @@
import { setupTestServer } from '@test-integration/utils';
import * as testDb from '../shared/testDb';
import { createMember, createOwner, getUserById } from '@test-integration/db/users';
import { mockInstance } from '@test/mocking';
import { Telemetry } from '@/telemetry';
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
describe('Users in Public API', () => {
const testServer = setupTestServer({ endpointGroups: ['publicApi'] });
mockInstance(Telemetry);
beforeAll(async () => {
await testDb.init();
});
beforeEach(async () => {
await testDb.truncate(['User']);
});
describe('POST /users', () => {
it('if not authenticated, should reject', async () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: false });
const payload = { email: 'test@test.com', role: 'global:admin' };
/**
* Act
*/
const response = await testServer.publicApiAgentFor(owner).post('/users').send(payload);
/**
* Assert
*/
expect(response.status).toBe(401);
});
it('if missing scope, should reject', async () => {
/**
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const member = await createMember({ withApiKey: true });
const payload = [{ email: 'test@test.com', role: 'global:admin' }];
/**
* Act
*/
const response = await testServer.publicApiAgentFor(member).post('/users').send(payload);
/**
* Assert
*/
expect(response.status).toBe(403);
expect(response.body).toHaveProperty('message', 'Forbidden');
});
it('should create a user', async () => {
/**
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const owner = await createOwner({ withApiKey: true });
const payload = [{ email: 'test@test.com', role: 'global:admin' }];
/**
* Act
*/
const response = await testServer.publicApiAgentFor(owner).post('/users').send(payload);
/**
* Assert
*/
expect(response.status).toBe(201);
expect(response.body).toHaveLength(1);
const [result] = response.body;
const { user: returnedUser, error } = result;
const payloadUser = payload[0];
expect(returnedUser).toHaveProperty('email', payload[0].email);
expect(typeof returnedUser.inviteAcceptUrl).toBe('string');
expect(typeof returnedUser.emailSent).toBe('boolean');
expect(error).toBe('');
const storedUser = await getUserById(returnedUser.id);
expect(returnedUser.id).toBe(storedUser.id);
expect(returnedUser.email).toBe(storedUser.email);
expect(returnedUser.email).toBe(payloadUser.email);
expect(storedUser.role).toBe(payloadUser.role);
});
});
describe('DELETE /users/:id', () => {
it('if not authenticated, should reject', async () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: false });
const member = await createMember();
/**
* Act
*/
const response = await testServer.publicApiAgentFor(owner).delete(`/users/${member.id}`);
/**
* Assert
*/
expect(response.status).toBe(401);
});
it('if missing scope, should reject', async () => {
/**
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const firstMember = await createMember({ withApiKey: true });
const secondMember = await createMember();
/**
* Act
*/
const response = await testServer
.publicApiAgentFor(firstMember)
.delete(`/users/${secondMember.id}`);
/**
* Assert
*/
expect(response.status).toBe(403);
expect(response.body).toHaveProperty('message', 'Forbidden');
});
it('should delete a user', async () => {
/**
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const owner = await createOwner({ withApiKey: true });
const member = await createMember();
/**
* Act
*/
const response = await testServer.publicApiAgentFor(owner).delete(`/users/${member.id}`);
/**
* Assert
*/
expect(response.status).toBe(204);
await expect(getUserById(member.id)).rejects.toThrow();
});
});
describe('PATCH /users/:id/role', () => {
it('if not authenticated, should reject', async () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: false });
const member = await createMember();
/**
* Act
*/
const response = await testServer.publicApiAgentFor(owner).patch(`/users/${member.id}/role`);
/**
* Assert
*/
expect(response.status).toBe(401);
});
it('if not licensed, should reject', async () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: true });
const member = await createMember();
const payload = { newRoleName: 'global:admin' };
/**
* Act
*/
const response = await testServer
.publicApiAgentFor(owner)
.patch(`/users/${member.id}/role`)
.send(payload);
/**
* Assert
*/
expect(response.status).toBe(403);
expect(response.body).toHaveProperty(
'message',
new FeatureNotLicensedError('feat:advancedPermissions').message,
);
});
it('if missing scope, should reject', async () => {
/**
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const firstMember = await createMember({ withApiKey: true });
const secondMember = await createMember();
const payload = { newRoleName: 'global:admin' };
/**
* Act
*/
const response = await testServer
.publicApiAgentFor(firstMember)
.patch(`/users/${secondMember.id}/role`)
.send(payload);
/**
* Assert
*/
expect(response.status).toBe(403);
expect(response.body).toHaveProperty('message', 'Forbidden');
});
it("should change a user's role", async () => {
/**
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const owner = await createOwner({ withApiKey: true });
const member = await createMember();
const payload = { newRoleName: 'global:admin' };
/**
* Act
*/
const response = await testServer
.publicApiAgentFor(owner)
.patch(`/users/${member.id}/role`)
.send(payload);
/**
* Assert
*/
expect(response.status).toBe(204);
const storedUser = await getUserById(member.id);
expect(storedUser.role).toBe(payload.newRoleName);
});
});
});