diff --git a/jest/int/setup-after-env.js b/jest/int/setup-after-env.js index b91a5b69..4b9c58d4 100644 --- a/jest/int/setup-after-env.js +++ b/jest/int/setup-after-env.js @@ -3,6 +3,7 @@ // test environment the max listeners limitation is lifted. require('events').EventEmitter.defaultMaxListeners = 1000; +require('../../dist/server/tests/int/mocks'); require('../../dist/server'); const {db} = require('../../dist/server/db'); diff --git a/src/components/middlewares/auth-zitadel.ts b/src/components/middlewares/auth-zitadel.ts index 44898abd..b9bfe13c 100644 --- a/src/components/middlewares/auth-zitadel.ts +++ b/src/components/middlewares/auth-zitadel.ts @@ -26,6 +26,7 @@ export const authZitadel = async (req: Request, res: Response, next: NextFunctio res.locals.userId = r1.userId; res.locals.login = r1.username; res.locals.serviceUser = r2.username; + res.locals.zitadelUserRole = r1.role; return next(); } } diff --git a/src/components/middlewares/ctx.ts b/src/components/middlewares/ctx.ts index f848c01c..aedb5908 100644 --- a/src/components/middlewares/ctx.ts +++ b/src/components/middlewares/ctx.ts @@ -13,6 +13,7 @@ export const ctx = async (req: Request, res: Response, next: NextFunction) => { onlyPublic, projectId, serviceUser, + zitadelUserRole, } = res.locals; const privatePermissions = resolvePrivatePermissions( @@ -30,6 +31,7 @@ export const ctx = async (req: Request, res: Response, next: NextFunction) => { privatePermissions, projectId: projectId || null, serviceUser, + zitadelUserRole, }); next(); diff --git a/src/configs/common.ts b/src/configs/common.ts index a3d094eb..c2bc8732 100644 --- a/src/configs/common.ts +++ b/src/configs/common.ts @@ -33,7 +33,7 @@ export default { tenantIdOverride: 'common', dlsEnabled: false, - accessServiceEnabled: false, + accessServiceEnabled: Utils.isTrueArg(Utils.getEnvVariable('ZITADEL')), accessBindingsServiceEnabled: false, masterToken: Utils.getEnvTokenVariable('MASTER_TOKEN'), diff --git a/src/configs/int-testing.ts b/src/configs/int-testing.ts index 8aebdc83..df8fa0b8 100644 --- a/src/configs/int-testing.ts +++ b/src/configs/int-testing.ts @@ -1,3 +1,8 @@ +import {AuthPolicy} from '@gravity-ui/expresskit'; import {AppConfig} from '@gravity-ui/nodekit'; -export default {} as Partial; +export default { + zitadelEnabled: true, + accessServiceEnabled: true, + appAuthPolicy: AuthPolicy.required, +} as Partial; diff --git a/src/registry/common/components/iam/utils.ts b/src/registry/common/components/iam/utils.ts index cbd03eb8..a7f448e5 100644 --- a/src/registry/common/components/iam/utils.ts +++ b/src/registry/common/components/iam/utils.ts @@ -1,5 +1,61 @@ +import {AppContext, AppError} from '@gravity-ui/nodekit'; import type {CheckOrganizationPermission, CheckProjectPermission} from './types'; +import {OrganizationPermission, ProjectPermission} from '../../../../components/iam'; +import {US_ERRORS} from '../../../../const'; +import {ZitadelUserRole} from '../../../../types/zitadel'; -export const checkOrganizationPermission: CheckOrganizationPermission = async () => {}; +const throwAccessServicePermissionDenied = () => { + throw new AppError(US_ERRORS.ACCESS_SERVICE_PERMISSION_DENIED, { + code: US_ERRORS.ACCESS_SERVICE_PERMISSION_DENIED, + }); +}; -export const checkProjectPermission: CheckProjectPermission = async () => {}; +export const checkOrganizationPermission: CheckOrganizationPermission = async (args: { + ctx: AppContext; + permission: OrganizationPermission; +}) => { + const {ctx, permission} = args; + const {zitadelUserRole: role} = ctx.get('info'); + + switch (permission) { + case OrganizationPermission.UseInstance: + break; + + case OrganizationPermission.ManageInstance: + if (role !== ZitadelUserRole.Admin) { + throwAccessServicePermissionDenied(); + } + break; + + case OrganizationPermission.CreateCollectionInRoot: + case OrganizationPermission.CreateWorkbookInRoot: + if (role !== ZitadelUserRole.Editor && role !== ZitadelUserRole.Admin) { + throwAccessServicePermissionDenied(); + } + break; + + default: + throwAccessServicePermissionDenied(); + } +}; + +export const checkProjectPermission: CheckProjectPermission = async (args: { + ctx: AppContext; + permission: ProjectPermission; +}) => { + const {ctx, permission} = args; + + const {zitadelUserRole: role} = ctx.get('info'); + + switch (permission) { + case ProjectPermission.CreateCollectionInRoot: + case ProjectPermission.CreateWorkbookInRoot: + if (role !== ZitadelUserRole.Editor && role !== ZitadelUserRole.Admin) { + throwAccessServicePermissionDenied(); + } + break; + + default: + throwAccessServicePermissionDenied(); + } +}; diff --git a/src/registry/common/entities/collection/collection.ts b/src/registry/common/entities/collection/collection.ts index b8f23cb1..ee7d1df8 100644 --- a/src/registry/common/entities/collection/collection.ts +++ b/src/registry/common/entities/collection/collection.ts @@ -1,7 +1,10 @@ import type {AppContext} from '@gravity-ui/nodekit'; import type {CollectionModel} from '../../../../db/models/new/collection'; +import {AppError} from '@gravity-ui/nodekit'; import {CollectionConstructor, CollectionInstance} from './types'; -import {Permissions} from '../../../../entities/collection/types'; +import {CollectionPermission, Permissions} from '../../../../entities/collection/types'; +import {US_ERRORS} from '../../../../const'; +import {ZitadelUserRole} from '../../../../types/zitadel'; export const Collection: CollectionConstructor = class Collection implements CollectionInstance { ctx: AppContext; @@ -13,14 +16,42 @@ export const Collection: CollectionConstructor = class Collection implements Col this.model = model; } + private getAllPermissions() { + const {zitadelUserRole: role} = this.ctx.get('info'); + + const isEditorOrAdmin = role === ZitadelUserRole.Editor || role === ZitadelUserRole.Admin; + + const permissions = { + listAccessBindings: true, + updateAccessBindings: isEditorOrAdmin, + createCollection: isEditorOrAdmin, + createWorkbook: isEditorOrAdmin, + limitedView: true, + view: true, + update: isEditorOrAdmin, + copy: isEditorOrAdmin, + move: isEditorOrAdmin, + delete: isEditorOrAdmin, + }; + + return permissions; + } + async register() {} - async checkPermission() {} + async checkPermission(args: { + parentIds: string[]; + permission: CollectionPermission; + }): Promise { + const permissions = this.getAllPermissions(); - async fetchAllPermissions() {} + if (permissions[args.permission] === false) { + throw new AppError(US_ERRORS.ACCESS_SERVICE_PERMISSION_DENIED, { + code: US_ERRORS.ACCESS_SERVICE_PERMISSION_DENIED, + }); + } - setPermissions(permissions: Permissions) { - this.permissions = permissions; + return Promise.resolve(); } enableAllPermissions() { @@ -37,4 +68,13 @@ export const Collection: CollectionConstructor = class Collection implements Col delete: true, }; } + + setPermissions(permissions: Permissions) { + this.permissions = permissions; + } + + async fetchAllPermissions() { + this.permissions = this.getAllPermissions(); + return Promise.resolve(); + } }; diff --git a/src/registry/common/entities/collection/types.ts b/src/registry/common/entities/collection/types.ts index 78251ef1..005c6f68 100644 --- a/src/registry/common/entities/collection/types.ts +++ b/src/registry/common/entities/collection/types.ts @@ -15,11 +15,11 @@ export interface CollectionInstance { checkPermission(args: {parentIds: string[]; permission: CollectionPermission}): Promise; - fetchAllPermissions(args: {parentIds: string[]}): Promise; - setPermissions(permissions: Permissions): void; enableAllPermissions(): void; + + fetchAllPermissions(args: {parentIds: string[]}): Promise; } export type BulkFetchCollectionsAllPermissions = ( diff --git a/src/registry/common/entities/collection/utils.ts b/src/registry/common/entities/collection/utils.ts index 5996f5fb..f932e371 100644 --- a/src/registry/common/entities/collection/utils.ts +++ b/src/registry/common/entities/collection/utils.ts @@ -7,7 +7,11 @@ export const bulkFetchCollectionsAllPermissions: BulkFetchCollectionsAllPermissi ) => { return items.map(({model}) => { const collection = new Collection({ctx, model}); - collection.enableAllPermissions(); + if (ctx.config.accessServiceEnabled) { + collection.fetchAllPermissions({parentIds: []}); + } else { + collection.enableAllPermissions(); + } return collection; }); }; diff --git a/src/registry/common/entities/workbook/utils.ts b/src/registry/common/entities/workbook/utils.ts index 715f1924..7834f174 100644 --- a/src/registry/common/entities/workbook/utils.ts +++ b/src/registry/common/entities/workbook/utils.ts @@ -7,7 +7,11 @@ export const bulkFetchWorkbooksAllPermissions: BulkFetchWorkbooksAllPermissions ) => { return items.map(({model}) => { const workbook = new Workbook({ctx, model}); - workbook.enableAllPermissions(); + if (ctx.config.accessServiceEnabled) { + workbook.fetchAllPermissions({parentIds: []}); + } else { + workbook.enableAllPermissions(); + } return workbook; }); }; diff --git a/src/registry/common/entities/workbook/workbook.ts b/src/registry/common/entities/workbook/workbook.ts index ff170722..fe1de9f6 100644 --- a/src/registry/common/entities/workbook/workbook.ts +++ b/src/registry/common/entities/workbook/workbook.ts @@ -1,7 +1,10 @@ import type {AppContext} from '@gravity-ui/nodekit'; import type {WorkbookModel} from '../../../../db/models/new/workbook'; +import {AppError} from '@gravity-ui/nodekit'; import {WorkbookConstructor, WorkbookInstance} from './types'; -import {Permissions} from '../../../../entities/workbook/types'; +import {Permissions, WorkbookPermission} from '../../../../entities/workbook/types'; +import {US_ERRORS} from '../../../../const'; +import {ZitadelUserRole} from '../../../../types/zitadel'; export const Workbook: WorkbookConstructor = class Workbook implements WorkbookInstance @@ -15,11 +18,50 @@ export const Workbook: WorkbookConstructor = class Workbook this.model = model; } - async register() {} + private getAllPermissions() { + const {zitadelUserRole: role} = this.ctx.get('info'); - async checkPermission() {} + const isEditorOrAdmin = role === ZitadelUserRole.Editor || role === ZitadelUserRole.Admin; - async fetchAllPermissions() {} + const permissions = { + listAccessBindings: true, + updateAccessBindings: isEditorOrAdmin, + limitedView: true, + view: true, + update: isEditorOrAdmin, + copy: isEditorOrAdmin, + move: isEditorOrAdmin, + publish: isEditorOrAdmin, + embed: isEditorOrAdmin, + delete: isEditorOrAdmin, + }; + + return permissions; + } + + async register(_args: {parentIds: string[]}): Promise { + return Promise.resolve(); + } + + async checkPermission(args: { + parentIds: string[]; + permission: WorkbookPermission; + }): Promise { + const permissions = this.getAllPermissions(); + + if (permissions[args.permission] === false) { + throw new AppError(US_ERRORS.ACCESS_SERVICE_PERMISSION_DENIED, { + code: US_ERRORS.ACCESS_SERVICE_PERMISSION_DENIED, + }); + } + + return Promise.resolve(); + } + + async fetchAllPermissions(): Promise { + this.permissions = this.getAllPermissions(); + return Promise.resolve(); + } setPermissions(permissions: Permissions) { this.permissions = permissions; diff --git a/src/services/new/workbook/get-workbooks-list-by-ids.ts b/src/services/new/workbook/get-workbooks-list-by-ids.ts index 3bcd2634..6ba57baa 100644 --- a/src/services/new/workbook/get-workbooks-list-by-ids.ts +++ b/src/services/new/workbook/get-workbooks-list-by-ids.ts @@ -86,9 +86,7 @@ export const getWorkbooksListByIds = async ( if (includePermissionsInfo) { return workbookList.map((model) => { const workbook = new Workbook({ctx, model}); - workbook.enableAllPermissions(); - return workbook; }); } diff --git a/src/services/new/workbook/get-workbooks-list.ts b/src/services/new/workbook/get-workbooks-list.ts index bc999c12..97462866 100644 --- a/src/services/new/workbook/get-workbooks-list.ts +++ b/src/services/new/workbook/get-workbooks-list.ts @@ -207,11 +207,9 @@ export const getWorkbooksList = async ( } else { workbooks = workbooksPage.results.map((model) => { const workbook = new Workbook({ctx, model}); - if (includePermissionsInfo) { workbook.enableAllPermissions(); } - return workbook; }); } diff --git a/src/tests/int/constants.ts b/src/tests/int/constants.ts index 027e49e5..a466894c 100644 --- a/src/tests/int/constants.ts +++ b/src/tests/int/constants.ts @@ -5,3 +5,5 @@ export const testUserLogin = 'unknown'; export const testTenantId = 'common'; export const testProjectId = null; + +export const ZITADEL_USER_ROLE_HEADER = 'zitadel-user-role'; diff --git a/src/tests/int/mocks/auth-zitadel.ts b/src/tests/int/mocks/auth-zitadel.ts new file mode 100644 index 00000000..508fca61 --- /dev/null +++ b/src/tests/int/mocks/auth-zitadel.ts @@ -0,0 +1,17 @@ +import {Request, Response, NextFunction} from '@gravity-ui/expresskit'; + +jest.mock('../../../components/middlewares/auth-zitadel', () => { + const originalModule = jest.requireActual('../../../components/middlewares/auth-zitadel'); + + return { + ...originalModule, + + authZitadel: jest.fn((req: Request, res: Response, next: NextFunction) => { + const {ZITADEL_USER_ROLE_HEADER} = require('../constants'); + const {ZitadelUserRole} = require('../../../types/zitadel'); + const role = req.headers[ZITADEL_USER_ROLE_HEADER]; + res.locals.zitadelUserRole = role ?? ZitadelUserRole.Viewer; + return next(); + }), + }; +}); diff --git a/src/tests/int/mocks/index.ts b/src/tests/int/mocks/index.ts new file mode 100644 index 00000000..416ffbc6 --- /dev/null +++ b/src/tests/int/mocks/index.ts @@ -0,0 +1 @@ +import './auth-zitadel'; diff --git a/src/tests/int/suites/entries/copy.test.ts b/src/tests/int/suites/entries/copy.test.ts index 0d4c7feb..acf4c5d9 100644 --- a/src/tests/int/suites/entries/copy.test.ts +++ b/src/tests/int/suites/entries/copy.test.ts @@ -1,6 +1,8 @@ import request from 'supertest'; import usApp from '../../../..'; import {auth} from '../../utils'; +import {ZITADEL_USER_ROLE_HEADER} from '../../constants'; +import {ZitadelUserRole} from '../../../../types/zitadel'; const app = usApp.express; @@ -31,7 +33,19 @@ describe('Copy entries', () => { }); test('Create entries - [POST /v1/entries]', async () => { + await auth(request(app).post('/v1/entries')) + .send({ + scope: 'dataset', + type: 'graph', + meta: {}, + data: {}, + name: 'EntryName1', + workbookId: workbookId1, + }) + .expect(403); + const {body: entry1Body} = await auth(request(app).post('/v1/entries')) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) .send({ scope: 'dataset', type: 'graph', @@ -43,6 +57,7 @@ describe('Copy entries', () => { .expect(200); const {body: entry2Body} = await auth(request(app).post('/v1/entries')) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) .send({ scope: 'dataset', type: 'graph', @@ -58,7 +73,15 @@ describe('Copy entries', () => { }); test('Copy entries - [POST /v2/copy-entries]', async () => { + await auth(request(app).post('/v2/copy-entries')) + .send({ + entryIds: [workbookId1EntryId1, workbookId1EntryId2], + workbookId: workbookId2, + }) + .expect(403); + const {body} = await auth(request(app).post('/v2/copy-entries')) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) .send({ entryIds: [workbookId1EntryId1, workbookId1EntryId2], workbookId: workbookId2, @@ -83,6 +106,16 @@ describe('Copy entries', () => { test('Copy entries with duplicate names - [POST /v2/copy-entries]', async () => { await auth(request(app).post('/v2/copy-entries')) + .send({ + entryIds: [workbookId1EntryId1, workbookId1EntryId2], + workbookId: workbookId2, + }) + .expect(403); + + await auth(request(app).post('/v2/copy-entries')) + .set({ + [ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor, + }) .send({ entryIds: [workbookId1EntryId1, workbookId1EntryId2], workbookId: workbookId2, @@ -106,9 +139,23 @@ describe('Copy entries', () => { }); test('Copy entries with incremented duplicate names - [POST /v2/copy-entries]', async () => { + await auth(request(app).post('/v1/entries')) + .send({ + scope: 'dataset', + type: 'graph', + meta: {}, + data: {}, + name: 'EntryName1 (COPY 1)', + workbookId: workbookId1, + }) + .expect(403); + const { body: {entryId: workbookId1EntryId3}, } = await auth(request(app).post('/v1/entries')) + .set({ + [ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor, + }) .send({ scope: 'dataset', type: 'graph', @@ -120,6 +167,9 @@ describe('Copy entries', () => { .expect(200); await auth(request(app).post('/v2/copy-entries')) + .set({ + [ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor, + }) .send({ entryIds: [workbookId1EntryId3], workbookId: workbookId2, @@ -144,7 +194,18 @@ describe('Copy entries', () => { }); test('Delete workbooks - [DELETE /v2/workbooks/:workbookId]', async () => { - await auth(request(app).delete(`/v2/workbooks/${workbookId1}`)).expect(200); - await auth(request(app).delete(`/v2/workbooks/${workbookId2}`)).expect(200); + await auth(request(app).delete(`/v2/workbooks/${workbookId1}`)).expect(403); + await auth(request(app).delete(`/v2/workbooks/${workbookId2}`)).expect(403); + + await auth(request(app).delete(`/v2/workbooks/${workbookId1}`)) + .set({ + [ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor, + }) + .expect(200); + await auth(request(app).delete(`/v2/workbooks/${workbookId2}`)) + .set({ + [ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor, + }) + .expect(200); }); }); diff --git a/src/tests/int/suites/workbooks.private.test.ts b/src/tests/int/suites/workbooks.private.test.ts index 73ddb77f..b6f959f8 100644 --- a/src/tests/int/suites/workbooks.private.test.ts +++ b/src/tests/int/suites/workbooks.private.test.ts @@ -1,8 +1,9 @@ import request from 'supertest'; -import {systemId, testTenantId, testProjectId} from '../constants'; +import {systemId, testTenantId, testProjectId, ZITADEL_USER_ROLE_HEADER} from '../constants'; import {US_MASTER_TOKEN_HEADER} from '../../../const'; import usApp from '../../..'; import {auth} from '../utils'; +import {ZitadelUserRole} from '../../../types/zitadel'; const app = usApp.express; const masterToken = usApp.config.masterToken[0]; @@ -67,7 +68,13 @@ describe('Private Workbooks managment', () => { workbookId: expect.any(String), }); - await auth(request(app).delete(`/v2/workbooks/${workbooksData.id}`)).expect(200); + await auth(request(app).delete(`/v2/workbooks/${workbooksData.id}`)).expect(403); + + await auth(request(app).delete(`/v2/workbooks/${workbooksData.id}`)) + .set({ + [ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor, + }) + .expect(200); }); }); @@ -91,6 +98,20 @@ describe('Private Entries in workboooks managment', () => { testWorkbookId = bodyWorkbook.workbookId; await auth(request(app).post('/v1/entries')) + .send({ + scope: 'dataset', + type: 'graph', + meta: {}, + data: {}, + name: entry1Name, + workbookId: testWorkbookId, + }) + .expect(403); + + await auth(request(app).post('/v1/entries')) + .set({ + [ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor, + }) .send({ scope: 'dataset', type: 'graph', @@ -136,7 +157,13 @@ describe('Private Entries in workboooks managment', () => { ]), }); - await auth(request(app).delete(`/v2/workbooks/${testWorkbookId}`)).expect(200); + await auth(request(app).delete(`/v2/workbooks/${testWorkbookId}`)).expect(403); + + await auth(request(app).delete(`/v2/workbooks/${testWorkbookId}`)) + .set({ + [ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor, + }) + .expect(200); }); }); @@ -160,6 +187,20 @@ describe('Private for one workboook managment', () => { testWorkbookId = bodyWorkbook.workbookId; await auth(request(app).post('/v1/entries')) + .send({ + scope: 'dataset', + type: 'graph', + meta: {}, + data: {}, + name: entry1Name, + workbookId: testWorkbookId, + }) + .expect(403); + + await auth(request(app).post('/v1/entries')) + .set({ + [ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor, + }) .send({ scope: 'dataset', type: 'graph', @@ -172,7 +213,11 @@ describe('Private for one workboook managment', () => { }); test('Restore workbook with entries – [POST /private/v2/workbooks/:workbookId/restore]', async () => { - await auth(request(app).delete(`/v2/workbooks/${testWorkbookId}`)).expect(200); + await auth(request(app).delete(`/v2/workbooks/${testWorkbookId}`)) + .set({ + [ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor, + }) + .expect(200); await request(app).post(`/private/v2/workbooks/${testWorkbookId}/restore`).expect(403); @@ -219,6 +264,12 @@ describe('Private for one workboook managment', () => { ]), }); - await auth(request(app).delete(`/v2/workbooks/${testWorkbookId}`)).expect(200); + await auth(request(app).delete(`/v2/workbooks/${testWorkbookId}`)).expect(403); + + await auth(request(app).delete(`/v2/workbooks/${testWorkbookId}`)) + .set({ + [ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor, + }) + .expect(200); }); }); diff --git a/src/tests/int/suites/workbooks.test.ts b/src/tests/int/suites/workbooks.test.ts index 85a50c33..bb38c211 100644 --- a/src/tests/int/suites/workbooks.test.ts +++ b/src/tests/int/suites/workbooks.test.ts @@ -1,8 +1,9 @@ import request from 'supertest'; -import {systemId, testTenantId, testProjectId} from '../constants'; +import {systemId, testTenantId, testProjectId, ZITADEL_USER_ROLE_HEADER} from '../constants'; import {US_MASTER_TOKEN_HEADER} from '../../../const'; import usApp from '../../..'; import {auth} from '../utils'; +import {ZitadelUserRole} from '../../../types/zitadel'; const app = usApp.express; const masterToken = usApp.config.masterToken[0]; @@ -102,6 +103,24 @@ describe('Workbooks managment', () => { workbookId: workbooksData[0].id, collectionId: null, }); + + const response1 = await auth(request(app).get(`/v2/workbooks/${workbooksData[0].id}`)) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) + .expect(200); + + expect(response1.body).toStrictEqual({ + createdAt: expect.any(String), + createdBy: systemId, + updatedAt: expect.any(String), + updatedBy: systemId, + description: workbooksData[0].description, + meta: {}, + projectId: testProjectId, + tenantId: testTenantId, + title: workbooksData[0].title, + workbookId: workbooksData[0].id, + collectionId: null, + }); }); test('Get list of workbooks – [GET /v2/workbooks]', async () => { @@ -139,6 +158,41 @@ describe('Workbooks managment', () => { }, ]), }); + + const response2 = await auth(request(app).get(`/v2/workbooks`)) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) + .expect(200); + + expect(response2.body).toStrictEqual({ + workbooks: expect.arrayContaining([ + { + createdAt: expect.any(String), + createdBy: systemId, + updatedAt: expect.any(String), + updatedBy: systemId, + description: workbooksData[1].description, + meta: {}, + projectId: testProjectId, + tenantId: testTenantId, + title: workbooksData[1].title, + workbookId: workbooksData[1].id, + collectionId: null, + }, + { + createdAt: expect.any(String), + createdBy: systemId, + updatedAt: expect.any(String), + updatedBy: systemId, + description: workbooksData[0].description, + meta: {}, + projectId: testProjectId, + tenantId: testTenantId, + title: workbooksData[0].title, + workbookId: workbooksData[0].id, + collectionId: null, + }, + ]), + }); }); test('Get list of workbooks with pagination – [GET /v2/workbooks]', async () => { @@ -204,9 +258,17 @@ describe('Workbooks managment', () => { workbooksData[0].title = 'Renamed test workbook title 1'; workbooksData[0].description = 'Renamed test workbook description 1'; + await auth(request(app).post(`/v2/workbooks/${workbooksData[0].id}/update`)) + .send({ + title: workbooksData[0].title, + description: workbooksData[0].description, + }) + .expect(403); + const response = await auth( request(app).post(`/v2/workbooks/${workbooksData[0].id}/update`), ) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) .send({ title: workbooksData[0].title, description: workbooksData[0].description, @@ -231,8 +293,12 @@ describe('Workbooks managment', () => { }); test('Delete workbooks – [DELETE /v2/workbooks/:workbookId]', async () => { - await auth(request(app).delete(`/v2/workbooks/${workbooksData[0].id}`)).expect(200); - await auth(request(app).delete(`/v2/workbooks/${workbooksData[1].id}`)).expect(200); + await auth(request(app).delete(`/v2/workbooks/${workbooksData[0].id}`)) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) + .expect(200); + await auth(request(app).delete(`/v2/workbooks/${workbooksData[1].id}`)) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) + .expect(200); await auth(request(app).get(`/v2/workbooks/${workbooksData[0].id}`)).expect(404); await auth(request(app).get(`/v2/workbooks/${workbooksData[1].id}`)).expect(404); @@ -281,6 +347,7 @@ describe('Entries in workboooks managment', () => { testWorkbookId = bodyWorkbook.workbookId; const responseEntry1 = await auth(request(app).post('/v1/entries')) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) .send({ scope: 'dataset', type: 'graph', @@ -317,6 +384,7 @@ describe('Entries in workboooks managment', () => { }); const responseEntry2 = await auth(request(app).post('/v1/entries')) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) .send({ scope: 'dataset', type: 'graph', @@ -405,11 +473,18 @@ describe('Entries in workboooks managment', () => { test('Copy workbook with entries – [POST /v2/workbooks/:workbookId/copy]', async () => { const testNewTitle = 'Copied test workbook with entries title'; - const response = await auth(request(app).post(`/v2/workbooks/${testWorkbookId}/copy`)).send( - { + await auth(request(app).post(`/v2/workbooks/${testWorkbookId}/copy`)) + .send({ + newTitle: testNewTitle, + }) + .expect(403); + + const response = await auth(request(app).post(`/v2/workbooks/${testWorkbookId}/copy`)) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) + .send({ newTitle: testNewTitle, - }, - ); + }) + .expect(200); const {body} = response; @@ -482,8 +557,15 @@ describe('Entries in workboooks managment', () => { }); test('Delete workbooks with entries – [DELETE /v2/workbooks/:workbookId]', async () => { - await auth(request(app).delete(`/v2/workbooks/${testWorkbookId}`)).expect(200); - await auth(request(app).delete(`/v2/workbooks/${testCopiedWorkbookId}`)).expect(200); + await auth(request(app).delete(`/v2/workbooks/${testWorkbookId}`)).expect(403); + await auth(request(app).delete(`/v2/workbooks/${testCopiedWorkbookId}`)).expect(403); + + await auth(request(app).delete(`/v2/workbooks/${testWorkbookId}`)) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) + .expect(200); + await auth(request(app).delete(`/v2/workbooks/${testCopiedWorkbookId}`)) + .set({[ZITADEL_USER_ROLE_HEADER]: ZitadelUserRole.Editor}) + .expect(200); await auth(request(app).get(`/v2/workbooks/${testWorkbookId}`)).expect(404); await auth(request(app).get(`/v2/workbooks/${testCopiedWorkbookId}`)).expect(404); diff --git a/src/types/ctx.ts b/src/types/ctx.ts index d95e58e8..677fae67 100644 --- a/src/types/ctx.ts +++ b/src/types/ctx.ts @@ -2,7 +2,7 @@ import {EmbedModel} from '../db/models/new/embed'; import {JoinedEntryRevisionColumns} from '../db/presentations/joined-entry-revision'; import {EmbeddingToken} from '../types/embedding'; import {PrivatePermissions} from './models'; -import {ZitadelServiceUser} from './zitadel'; +import {ZitadelServiceUser, ZitadelUserRole} from './zitadel'; export type UserCtxInfo = { userId: string; @@ -28,4 +28,5 @@ export type CtxInfo = { projectId: string | null; embeddingInfo?: EmbeddingInfo; serviceUser?: ZitadelServiceUser; + zitadelUserRole?: ZitadelUserRole; }; diff --git a/src/types/zitadel.ts b/src/types/zitadel.ts index abf4a298..823e0bc0 100644 --- a/src/types/zitadel.ts +++ b/src/types/zitadel.ts @@ -2,3 +2,9 @@ export enum ZitadelServiceUser { charts = 'charts', bi = 'bi', } + +export enum ZitadelUserRole { + Editor = 'datalens.editor', + Admin = 'datalens.admin', + Viewer = 'datalens.viewer', +} diff --git a/src/utils/zitadel.ts b/src/utils/zitadel.ts index 4c115299..4ebb9135 100644 --- a/src/utils/zitadel.ts +++ b/src/utils/zitadel.ts @@ -2,12 +2,7 @@ import {AppContext} from '@gravity-ui/nodekit'; import {Utils} from './utils'; import axios from 'axios'; import axiosRetry from 'axios-retry'; - -enum ZitadelUserRole { - Creator = 'creator', - Admin = 'admin', - Viewer = 'viewer', -} +import {ZitadelUserRole} from '../types/zitadel'; type IntrospectionResult = { active: boolean; @@ -28,12 +23,12 @@ const getRole = (data: any): ZitadelUserRole => { return ZitadelUserRole.Viewer; } - if (roles['admin']) { + if (roles[ZitadelUserRole.Admin]) { return ZitadelUserRole.Admin; } - if (roles['creator']) { - return ZitadelUserRole.Creator; + if (roles[ZitadelUserRole.Editor]) { + return ZitadelUserRole.Editor; } return ZitadelUserRole.Viewer;