mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
feat(core): Support create, delete, edit role for users in Public API (#10279)
This commit is contained in:
parent
a533916628
commit
84efbd9b9c
|
@ -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'
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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:
|
||||
|
|
252
packages/cli/test/integration/publicApi/users.test.ts
Normal file
252
packages/cli/test/integration/publicApi/users.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue