diff --git a/packages/backend/src/controllers/api/v1/admin/roles/create-role.ee.js b/packages/backend/src/controllers/api/v1/admin/roles/create-role.ee.js new file mode 100644 index 00000000..02ae7b82 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/roles/create-role.ee.js @@ -0,0 +1,25 @@ +import kebabCase from 'lodash/kebabCase.js'; +import { renderObject } from '../../../../../helpers/renderer.js'; +import Role from '../../../../../models/role.js'; + +export default async (request, response) => { + const roleData = roleParams(request); + + const roleWithPermissions = await Role.query().insertGraphAndFetch(roleData, { + relate: ['permissions'], + }); + + renderObject(response, roleWithPermissions, { status: 201 }); +}; + +const roleParams = (request) => { + const { name, description, permissions } = request.body; + const key = kebabCase(name); + + return { + key, + name, + description, + permissions, + }; +}; diff --git a/packages/backend/src/controllers/api/v1/admin/roles/create-role.ee.test.js b/packages/backend/src/controllers/api/v1/admin/roles/create-role.ee.test.js new file mode 100644 index 00000000..e31df92d --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/roles/create-role.ee.test.js @@ -0,0 +1,108 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app.js'; +import Role from '../../../../../models/role.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import createRoleMock from '../../../../../../test/mocks/rest/api/v1/admin/roles/create-role.ee.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('POST /api/v1/admin/roles', () => { + let role, currentUser, token; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + role = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: role.id }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the created role along with permissions', async () => { + const roleData = { + name: 'Viewer', + description: '', + permissions: [ + { + action: 'read', + subject: 'Flow', + conditions: ['isCreator'], + }, + ], + }; + + const response = await request(app) + .post('/api/v1/admin/roles') + .set('Authorization', token) + .send(roleData) + .expect(201); + + const createdRole = await Role.query() + .withGraphFetched({ permissions: true }) + .findOne({ key: 'viewer' }) + .throwIfNotFound(); + + const expectedPayload = await createRoleMock( + { + ...createdRole, + ...roleData, + isAdmin: createdRole.isAdmin, + }, + [ + { + ...createdRole.permissions[0], + ...roleData.permissions[0], + }, + ] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return unprocessable entity response for invalid data', async () => { + const roleData = { + description: '', + permissions: [], + }; + + const response = await request(app) + .post('/api/v1/admin/roles') + .set('Authorization', token) + .send(roleData) + .expect(422); + + expect(response.body).toStrictEqual({ + errors: { + name: ["must have required property 'name'"], + key: ['must NOT have fewer than 1 characters'], + }, + meta: { + type: 'ModelValidation', + }, + }); + }); + + it('should return unprocessable entity response for duplicate role', async () => { + const roleData = { + name: 'admin', + permissions: [], + }; + + const response = await request(app) + .post('/api/v1/admin/roles') + .set('Authorization', token) + .send(roleData) + .expect(422); + + expect(response.body).toStrictEqual({ + errors: { + key: ["'key' must be unique."], + }, + meta: { + type: 'UniqueViolationError', + }, + }); + }); +}); diff --git a/packages/backend/src/routes/api/v1/admin/roles.ee.js b/packages/backend/src/routes/api/v1/admin/roles.ee.js index 238856e8..6ec88967 100644 --- a/packages/backend/src/routes/api/v1/admin/roles.ee.js +++ b/packages/backend/src/routes/api/v1/admin/roles.ee.js @@ -2,11 +2,20 @@ import { Router } from 'express'; import { authenticateUser } from '../../../../helpers/authentication.js'; import { authorizeAdmin } from '../../../../helpers/authorization.js'; import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js'; +import createRoleAction from '../../../../controllers/api/v1/admin/roles/create-role.ee.js'; import getRolesAction from '../../../../controllers/api/v1/admin/roles/get-roles.ee.js'; import getRoleAction from '../../../../controllers/api/v1/admin/roles/get-role.ee.js'; const router = Router(); +router.post( + '/', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + createRoleAction +); + router.get( '/', authenticateUser, diff --git a/packages/backend/test/mocks/rest/api/v1/admin/roles/create-role.ee.js b/packages/backend/test/mocks/rest/api/v1/admin/roles/create-role.ee.js new file mode 100644 index 00000000..9bcd604d --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/roles/create-role.ee.js @@ -0,0 +1,33 @@ +const createRoleMock = async (role, permissions = []) => { + const data = { + id: role.id, + key: role.key, + name: role.name, + isAdmin: role.isAdmin, + description: role.description, + createdAt: role.createdAt.getTime(), + updatedAt: role.updatedAt.getTime(), + permissions: permissions.map((permission) => ({ + id: permission.id, + action: permission.action, + conditions: permission.conditions, + roleId: permission.roleId, + subject: permission.subject, + createdAt: permission.createdAt.getTime(), + updatedAt: permission.updatedAt.getTime(), + })), + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Role', + }, + }; +}; + +export default createRoleMock;