From f7ac33edf89c1f02371456404bcee2acbd9a0f07 Mon Sep 17 00:00:00 2001 From: underscope Date: Tue, 28 Nov 2023 09:31:11 +0100 Subject: [PATCH 01/40] Enable routes for integration user - If flag has been set --- server/repository/index.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/server/repository/index.js b/server/repository/index.js index e7ef16ca0..fb2f4e353 100644 --- a/server/repository/index.js +++ b/server/repository/index.js @@ -10,6 +10,7 @@ import path from 'node:path'; import processQuery from '../shared/util/processListQuery.js'; import proxy from './proxy.js'; import proxyMw from '../shared/storage/proxy/mw.js'; +import roleConfig from '../../config/shared/role.js'; import storage from './storage.js'; /* eslint-disable */ @@ -17,6 +18,7 @@ import activity from '../activity/index.js'; import comment from '../comment/index.js'; import revision from '../revision/index.js'; import contentElement from '../content-element/index.js'; +const { user: role } = roleConfig; import storageRouter from '../shared/storage/storage.router.js'; /* eslint-enable */ @@ -24,12 +26,19 @@ const { Repository } = db; const router = express.Router(); const { setSignedCookies } = proxyMw(storage, proxy); +const { + EXTERNAL_ACCESS_MANAGEMENT: isExternalAccessManagement +} = process.env; + +const authorizeAdminUser = isExternalAccessManagement + ? authorize(role.INTEGRATION) + : authorize(); // NOTE: disk storage engine expects an object to be passed as the first argument // https://github.com/expressjs/multer/blob/6b5fff5/storage/disk.js#L17-L18 const upload = multer({ storage: multer.diskStorage({}) }); router - .post('/import', authorize(), upload.single('archive'), ctrl.import); + .post('/import', authorizeAdminUser, upload.single('archive'), ctrl.import); router .param('repositoryId', getRepository) @@ -46,15 +55,15 @@ router.route('/:repositoryId') router .post('/:repositoryId/pin', ctrl.pin) - .post('/:repositoryId/clone', authorize(), ctrl.clone) + .post('/:repositoryId/clone', authorizeAdminUser, ctrl.clone) .post('/:repositoryId/publish', ctrl.publishRepoInfo) - .get('/:repositoryId/users', ctrl.getUsers) + .get('/:repositoryId/users', authorizeAdminUser, ctrl.getUsers) .get('/:repositoryId/export/setup', ctrl.initiateExportJob) .post('/:repositoryId/export/:jobId', ctrl.export) - .post('/:repositoryId/users', ctrl.upsertUser) - .delete('/:repositoryId/users/:userId', ctrl.removeUser) - .post('/:repositoryId/tags', ctrl.addTag) - .delete('/:repositoryId/tags/:tagId', ctrl.removeTag); + .post('/:repositoryId/users', authorizeAdminUser, ctrl.upsertUser) + .delete('/:repositoryId/users/:userId', authorizeAdminUser, ctrl.removeUser) + .post('/:repositoryId/tags', authorizeAdminUser, ctrl.addTag) + .delete('/:repositoryId/tags/:tagId', authorizeAdminUser, ctrl.removeTag); mount(router, '/:repositoryId', feed); mount(router, '/:repositoryId', activity); From c63995b4fd6f2561c059a1623e43a502d7b65d8c Mon Sep 17 00:00:00 2001 From: underscope Date: Tue, 28 Nov 2023 09:49:05 +0100 Subject: [PATCH 02/40] Disable UI options for external access management --- client/components/catalog/Container.vue | 4 ++++ client/components/common/Navbar.vue | 2 ++ client/components/repository/Settings/Sidebar.vue | 15 ++++++++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/client/components/catalog/Container.vue b/client/components/catalog/Container.vue index fce3773f7..6b1ea6e75 100644 --- a/client/components/catalog/Container.vue +++ b/client/components/catalog/Container.vue @@ -84,6 +84,7 @@ export default { name: 'catalog-container', data: () => ({ loading: true }), computed: { + isExternalAccessManagement: () => process.env.EXTERNAL_ACCESS_MANAGEMENT, ...mapState('repositories', { sortBy: state => state.$internals.sort, repositoryFilter: 'repositoryFilter', @@ -153,6 +154,9 @@ export default { } }, created() { + if (this.isExternalAccessManagement) { + this.$router.go(-1); + } // repositories must be reloaded for publishing badge to work properly // reset state manually to trigger "infinite" event in all cases this.resetPagination(); diff --git a/client/components/common/Navbar.vue b/client/components/common/Navbar.vue index a71eba922..c9cf9e513 100644 --- a/client/components/common/Navbar.vue +++ b/client/components/common/Navbar.vue @@ -63,11 +63,13 @@ export default { ...mapGetters('repository', ['repository']), title: () => BRAND_CONFIG.TITLE, logo: () => BRAND_CONFIG.LOGO_FULL, + isExternalAccessManagement: () => process.env.EXTERNAL_ACCESS_MANAGEMENT, routes() { const items = [ { name: 'Catalog', to: { name: 'catalog' } }, { name: 'Admin', to: { name: 'system-user-management' } } ]; + if (this.isExternalAccessManagement) items.shift(); if (!this.isAdmin) items.pop(); if (this.repository) { items.unshift({ diff --git a/client/components/repository/Settings/Sidebar.vue b/client/components/repository/Settings/Sidebar.vue index 5c020e677..18fe0b13f 100644 --- a/client/components/repository/Settings/Sidebar.vue +++ b/client/components/repository/Settings/Sidebar.vue @@ -43,20 +43,29 @@ export default { name: 'repository-settings-sidebar', computed: { + isExternalAccessManagement: () => process.env.EXTERNAL_ACCESS_MANAGEMENT, routes() { const { query } = this.$route; - return [ + const entries = [ { label: 'General', name: 'repository-info', icon: 'wrench' }, { label: 'People', name: 'user-management', icon: 'account' } ].map(route => ({ ...route, query })); + if (this.isExternalAccessManagement) entries.pop(); + return entries; }, actions() { - return [ - { label: 'Clone', icon: 'content-copy', name: 'clone' }, + const defaultEntries = [ { label: 'Publish', icon: 'upload', name: 'publish' }, { label: 'Export', icon: 'export', name: 'export' }, { label: 'Delete', icon: 'delete', name: 'delete', color: 'error' } ]; + const conditionalEntries = this.isExternallyManaged + ? [] + : [{ label: 'Clone', icon: 'content-copy', name: 'clone' }]; + return [ + ...defaultEntries, + ...conditionalEntries + ]; } } }; From cc44a53671804d8f5ecfedb31532de9e0d970de5 Mon Sep 17 00:00:00 2001 From: underscope Date: Tue, 28 Nov 2023 09:49:58 +0100 Subject: [PATCH 03/40] Expose new env variable --- .env.example | 3 +++ vite.config.mjs | 1 + 2 files changed, 4 insertions(+) diff --git a/.env.example b/.env.example index bbeef070d..cf354ea1b 100644 --- a/.env.example +++ b/.env.example @@ -125,6 +125,9 @@ REDIS_PORT=6379 REDIS_HOST=localhost REDIS_PASSWORD= +# External repository access management +EXTERNAL_ACCESS_MANAGEMENT=false + # Cypress CYPRESS_USERNAME=admin1@example.com CYPRESS_PASSWORD=admin123. diff --git a/vite.config.mjs b/vite.config.mjs index 6b740ef42..1c4301cce 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -16,6 +16,7 @@ const getDefine = env => ({ 'process.env.OIDC_ENABLED': yn(env.OIDC_ENABLED), 'process.env.OIDC_LOGOUT_ENABLED': yn(env.OIDC_LOGOUT_ENABLED), 'process.env.OIDC_LOGIN_TEXT': JSON.stringify(env.OIDC_LOGIN_TEXT), + 'process.env.EXTERNAL_ACCESS_MANAGEMENT': yn(env.EXTERNAL_ACCESS_MANAGEMENT), 'BRAND_CONFIG.TITLE': JSON.stringify(brandConfig.title), 'BRAND_CONFIG.FAVICON': JSON.stringify(brandConfig.favicon), 'BRAND_CONFIG.LOGO_COMPACT': JSON.stringify(brandConfig.logo.compact), From ff36e2d323f213a0df8b62ec08b6264a68043c38 Mon Sep 17 00:00:00 2001 From: underscope Date: Tue, 28 Nov 2023 09:57:53 +0100 Subject: [PATCH 04/40] Enable integration user on user routes --- server/user/index.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/server/user/index.js b/server/user/index.js index 195d481d0..11c2ac518 100644 --- a/server/user/index.js +++ b/server/user/index.js @@ -7,9 +7,19 @@ import db from '../shared/database/index.js'; import express from 'express'; import { processPagination } from '../shared/database/pagination.js'; import { requestLimiter } from '../shared/request/mw.js'; +import roleConfig from '../../config/shared/role.js'; const { User } = db; const router = express.Router(); +const { user: role } = roleConfig; + +const { + EXTERNAL_ACCESS_MANAGEMENT: isExternalAccessManagement +} = process.env; + +const authorizeAdminUser = isExternalAccessManagement + ? authorize(role.INTEGRATION) + : authorize(); // Public routes: router @@ -29,14 +39,14 @@ router // Protected routes: router .use(authService.authenticate('jwt')) - .get('/', authorize(), processPagination(User), ctrl.list) - .post('/', authorize(), ctrl.upsert) + .get('/', authorizeAdminUser, processPagination(User), ctrl.list) + .post('/', authorizeAdminUser, ctrl.upsert) .get('/logout', authService.logout()) .get('/me', ctrl.getProfile) .patch('/me', ctrl.updateProfile) .post('/me/change-password', ctrl.changePassword) - .delete('/:id', authorize(), ctrl.remove) - .post('/:id/reinvite', authorize(), ctrl.reinvite); + .delete('/:id', authorizeAdminUser, ctrl.remove) + .post('/:id/reinvite', authorizeAdminUser, ctrl.reinvite); export default { path: '/users', From 7b44f0b1bba77dfc88c6ca203ffd0a10e81ea3aa Mon Sep 17 00:00:00 2001 From: underscope Date: Tue, 28 Nov 2023 10:03:43 +0100 Subject: [PATCH 05/40] Enable creation for integration user --- server/repository/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/repository/index.js b/server/repository/index.js index fb2f4e353..3e28c8e67 100644 --- a/server/repository/index.js +++ b/server/repository/index.js @@ -46,7 +46,7 @@ router router.route('/') .get(processQuery({ limit: 100 }), ctrl.index) - .post(authorize(), ctrl.create); + .post(authorizeAdminUser, ctrl.create); router.route('/:repositoryId') .get(ctrl.get) From 5021df8da270807aa1425a0570ed2ba10b84aa20 Mon Sep 17 00:00:00 2001 From: underscope Date: Tue, 28 Nov 2023 10:06:25 +0100 Subject: [PATCH 06/40] =?UTF-8?q?=F0=9F=92=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/components/repository/Settings/Sidebar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/repository/Settings/Sidebar.vue b/client/components/repository/Settings/Sidebar.vue index 18fe0b13f..00b6cdea9 100644 --- a/client/components/repository/Settings/Sidebar.vue +++ b/client/components/repository/Settings/Sidebar.vue @@ -59,7 +59,7 @@ export default { { label: 'Export', icon: 'export', name: 'export' }, { label: 'Delete', icon: 'delete', name: 'delete', color: 'error' } ]; - const conditionalEntries = this.isExternallyManaged + const conditionalEntries = this.isExternalAccessManagement ? [] : [{ label: 'Clone', icon: 'content-copy', name: 'clone' }]; return [ From ec695c299eeda7c6ee325d199f9eb2d128ed6248 Mon Sep 17 00:00:00 2001 From: underscope Date: Wed, 29 Nov 2023 09:52:59 +0100 Subject: [PATCH 07/40] Add missing role --- config/shared/role.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/shared/role.js b/config/shared/role.js index 6436c22db..4343c48e2 100644 --- a/config/shared/role.js +++ b/config/shared/role.js @@ -1,7 +1,7 @@ import values from 'lodash/values.js'; const role = { - user: { USER: 'USER', ADMIN: 'ADMIN' }, + user: { USER: 'USER', ADMIN: 'ADMIN', INTEGRATION: 'INTEGRATION' }, repository: { ADMIN: 'ADMIN', AUTHOR: 'AUTHOR' } }; From 7abfd11125006380ec9fe303fce1a45e8d5779f3 Mon Sep 17 00:00:00 2001 From: underscope Date: Thu, 30 Nov 2023 16:15:13 +0100 Subject: [PATCH 08/40] Export integration user API --- ...tegration_user_API.postman_collection.json | 514 ++++++++++++++++++ 1 file changed, 514 insertions(+) create mode 100644 tools/integration_user_API.postman_collection.json diff --git a/tools/integration_user_API.postman_collection.json b/tools/integration_user_API.postman_collection.json new file mode 100644 index 000000000..8b00cba80 --- /dev/null +++ b/tools/integration_user_API.postman_collection.json @@ -0,0 +1,514 @@ +{ + "info": { + "_postman_id": "d2543cae-4168-45ba-bb48-3d4efc4f0d9f", + "name": "Tailor integration user API", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "31477572" + }, + "item": [ + { + "name": "repository", + "item": [ + { + "name": "user", + "item": [ + { + "name": "Get repository users", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/repositories/:id/users", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + ":id", + "users" + ], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Enable repository access", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"admin@example.com\",\n \"role\": \"ADMIN\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/repositories/:id/users", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + ":id", + "users" + ], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Revoke repository access", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base_url}}/repositories/:id/users/:userId", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + ":id", + "users", + ":userId" + ], + "variable": [ + { + "key": "id", + "value": "1" + }, + { + "key": "userId", + "value": "1" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "tag", + "item": [ + { + "name": "Add tag", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test tag\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/repositories/:id/tags", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + ":id", + "tags" + ], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Remove tag", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base_url}}/repositories/:id/tags/:tagId", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + ":id", + "tags", + ":tagId" + ], + "variable": [ + { + "key": "id", + "value": "" + }, + { + "key": "tagId", + "value": "" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "List repositories", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/info?id=1", + "host": [ + "{{base_url}}" + ], + "path": [ + "info" + ], + "query": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "List repositories with filter and pagination", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/info?id=1", + "host": [ + "{{base_url}}" + ], + "path": [ + "info" + ], + "query": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Get repository", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/info?id=1", + "host": [ + "{{base_url}}" + ], + "path": [ + "info" + ], + "query": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Create repository", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test\",\n \"description\": \"Test\",\n \"schema\": \"TEST_SCHEMA\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/repositories", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories" + ] + } + }, + "response": [] + }, + { + "name": "Clone repository", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test clone\",\n \"description\": \"Test clone\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/repositories/:id/clone", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + ":id", + "clone" + ], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "tag", + "item": [ + { + "name": "List", + "request": { + "method": "GET", + "header": [] + }, + "response": [] + } + ] + }, + { + "name": "user", + "item": [] + } + ] +} \ No newline at end of file From b39ec8682aabb960d92e63c94daacb0925affdc2 Mon Sep 17 00:00:00 2001 From: underscope Date: Fri, 1 Dec 2023 10:27:51 +0100 Subject: [PATCH 09/40] Update auth strategy --- server/shared/auth/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/shared/auth/index.js b/server/shared/auth/index.js index a1a8e6931..599bfc993 100644 --- a/server/shared/auth/index.js +++ b/server/shared/auth/index.js @@ -27,7 +27,8 @@ auth.use(new JwtStrategy({ audience: Audience.Scope.Access, jwtFromRequest: ExtractJwt.fromExtractors([ extractJwtFromCookie, - ExtractJwt.fromBodyField('token') + ExtractJwt.fromBodyField('token'), + ExtractJwt.fromHeader('access_token') ]), secretOrKey: config.jwt.secret }, verifyJWT)); From 2c69c16c9acc98948df4ef1ee1ab6ec02abbfa34 Mon Sep 17 00:00:00 2001 From: underscope Date: Fri, 1 Dec 2023 10:45:12 +0100 Subject: [PATCH 10/40] Update postman collection --- ...tegration_user_API.postman_collection.json | 1183 ++++++++++------- 1 file changed, 670 insertions(+), 513 deletions(-) diff --git a/tools/integration_user_API.postman_collection.json b/tools/integration_user_API.postman_collection.json index 8b00cba80..65484a37a 100644 --- a/tools/integration_user_API.postman_collection.json +++ b/tools/integration_user_API.postman_collection.json @@ -1,514 +1,671 @@ { - "info": { - "_postman_id": "d2543cae-4168-45ba-bb48-3d4efc4f0d9f", - "name": "Tailor integration user API", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "31477572" - }, - "item": [ - { - "name": "repository", - "item": [ - { - "name": "user", - "item": [ - { - "name": "Get repository users", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/repositories/:id/users", - "host": [ - "{{base_url}}" - ], - "path": [ - "repositories", - ":id", - "users" - ], - "variable": [ - { - "key": "id", - "value": "1" - } - ] - }, - "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." - }, - "response": [] - }, - { - "name": "Enable repository access", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"admin@example.com\",\n \"role\": \"ADMIN\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/repositories/:id/users", - "host": [ - "{{base_url}}" - ], - "path": [ - "repositories", - ":id", - "users" - ], - "variable": [ - { - "key": "id", - "value": "1" - } - ] - }, - "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." - }, - "response": [] - }, - { - "name": "Revoke repository access", - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [], - "url": { - "raw": "{{base_url}}/repositories/:id/users/:userId", - "host": [ - "{{base_url}}" - ], - "path": [ - "repositories", - ":id", - "users", - ":userId" - ], - "variable": [ - { - "key": "id", - "value": "1" - }, - { - "key": "userId", - "value": "1" - } - ] - } - }, - "response": [] - } - ] - }, - { - "name": "tag", - "item": [ - { - "name": "Add tag", - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Test tag\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/repositories/:id/tags", - "host": [ - "{{base_url}}" - ], - "path": [ - "repositories", - ":id", - "tags" - ], - "variable": [ - { - "key": "id", - "value": "1" - } - ] - } - }, - "response": [] - }, - { - "name": "Remove tag", - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "{{base_url}}/repositories/:id/tags/:tagId", - "host": [ - "{{base_url}}" - ], - "path": [ - "repositories", - ":id", - "tags", - ":tagId" - ], - "variable": [ - { - "key": "id", - "value": "" - }, - { - "key": "tagId", - "value": "" - } - ] - } - }, - "response": [] - } - ] - }, - { - "name": "List repositories", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/info?id=1", - "host": [ - "{{base_url}}" - ], - "path": [ - "info" - ], - "query": [ - { - "key": "id", - "value": "1" - } - ] - }, - "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." - }, - "response": [] - }, - { - "name": "List repositories with filter and pagination", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/info?id=1", - "host": [ - "{{base_url}}" - ], - "path": [ - "info" - ], - "query": [ - { - "key": "id", - "value": "1" - } - ] - }, - "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." - }, - "response": [] - }, - { - "name": "Get repository", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/info?id=1", - "host": [ - "{{base_url}}" - ], - "path": [ - "info" - ], - "query": [ - { - "key": "id", - "value": "1" - } - ] - }, - "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." - }, - "response": [] - }, - { - "name": "Create repository", - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Test\",\n \"description\": \"Test\",\n \"schema\": \"TEST_SCHEMA\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/repositories", - "host": [ - "{{base_url}}" - ], - "path": [ - "repositories" - ] - } - }, - "response": [] - }, - { - "name": "Clone repository", - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Test clone\",\n \"description\": \"Test clone\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/repositories/:id/clone", - "host": [ - "{{base_url}}" - ], - "path": [ - "repositories", - ":id", - "clone" - ], - "variable": [ - { - "key": "id", - "value": "1" - } - ] - } - }, - "response": [] - } - ] - }, - { - "name": "tag", - "item": [ - { - "name": "List", - "request": { - "method": "GET", - "header": [] - }, - "response": [] - } - ] - }, - { - "name": "user", - "item": [] - } - ] -} \ No newline at end of file + "info": { + "_postman_id": "d2543cae-4168-45ba-bb48-3d4efc4f0d9f", + "name": "Tailor integration user API", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "31477572" + }, + "item": [ + { + "name": "repository", + "item": [ + { + "name": "user", + "item": [ + { + "name": "Get repository users", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/repositories/:id/users", + "host": ["{{base_url}}"], + "path": ["repositories", ":id", "users"], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Enable repository access", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"admin@example.com\",\n \"role\": \"ADMIN\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/repositories/:id/users", + "host": ["{{base_url}}"], + "path": ["repositories", ":id", "users"], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Revoke repository access", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base_url}}/repositories/:id/users/:userId", + "host": ["{{base_url}}"], + "path": ["repositories", ":id", "users", ":userId"], + "variable": [ + { + "key": "id", + "value": "1" + }, + { + "key": "userId", + "value": "1" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "tag", + "item": [ + { + "name": "Add tag", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test tag\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/repositories/:id/tags", + "host": ["{{base_url}}"], + "path": ["repositories", ":id", "tags"], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Remove tag", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base_url}}/repositories/:id/tags/:tagId", + "host": ["{{base_url}}"], + "path": ["repositories", ":id", "tags", ":tagId"], + "variable": [ + { + "key": "id", + "value": "" + }, + { + "key": "tagId", + "value": "" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "List repositories", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/info?id=1", + "host": ["{{base_url}}"], + "path": ["info"], + "query": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "List repositories with filter and pagination", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/repositories?search=te&offset=0&limit=21&sortOrder=DESC&sortBy=createdAt&tagIds[]=1", + "host": ["{{base_url}}"], + "path": ["repositories"], + "query": [ + { + "key": "search", + "value": "te" + }, + { + "key": "offset", + "value": "0" + }, + { + "key": "limit", + "value": "21" + }, + { + "key": "sortOrder", + "value": "DESC" + }, + { + "key": "sortBy", + "value": "createdAt" + }, + { + "key": "tagIds[]", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Get repository", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/info?id=1", + "host": ["{{base_url}}"], + "path": ["info"], + "query": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Create repository", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test\",\n \"description\": \"Test\",\n \"schema\": \"TEST_SCHEMA\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/repositories", + "host": ["{{base_url}}"], + "path": ["repositories"] + } + }, + "response": [] + }, + { + "name": "Clone repository", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test clone\",\n \"description\": \"Test clone\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/repositories/:id/clone", + "host": ["{{base_url}}"], + "path": ["repositories", ":id", "clone"], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Import repository", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "archive", + "type": "file", + "src": "/Users/underscope/Desktop/test123.tgz" + }, + { + "key": "name", + "value": "Test import", + "type": "text" + }, + { + "key": "description", + "value": "Test import", + "type": "text" + } + ] + }, + "url": { + "raw": "{{base_url}}/repositories/import", + "host": ["{{base_url}}"], + "path": ["repositories", "import"] + } + }, + "response": [] + } + ] + }, + { + "name": "tag", + "item": [ + { + "name": "List", + "request": { + "method": "GET", + "header": [] + }, + "response": [] + } + ] + }, + { + "name": "user", + "item": [ + { + "name": "List", + "request": { + "method": "GET", + "header": [] + }, + "response": [] + }, + { + "name": "Upsert", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"test@gostudion.com\",\n \"firstName\": \"test\",\n \"lastName\": \"test\",\n \"role\": \"USER\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/users", + "host": ["{{base_url}}"], + "path": ["users"] + } + }, + "response": [] + }, + { + "name": "Delete", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"test@gostudion.com\",\n \"firstName\": \"test\",\n \"lastName\": \"test\",\n \"role\": \"USER\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/users/:id", + "host": ["{{base_url}}"], + "path": ["users", ":id"], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Reinvite", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"test@gostudion.com\",\n \"firstName\": \"test\",\n \"lastName\": \"test\",\n \"role\": \"USER\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/users/:id/reinvite", + "host": ["{{base_url}}"], + "path": ["users", ":id", "reinvite"], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + } + }, + "response": [] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [""] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [""] + } + } + ] +} From f9153a8c45053393c9589af12e786883748a839c Mon Sep 17 00:00:00 2001 From: underscope Date: Fri, 1 Dec 2023 10:56:56 +0100 Subject: [PATCH 11/40] Add readme --- tools/integration-user/README.md | 21 +++++++++++++++++++ ...tegration_user_API.postman_collection.json | 0 2 files changed, 21 insertions(+) create mode 100644 tools/integration-user/README.md rename tools/{ => integration-user}/integration_user_API.postman_collection.json (100%) diff --git a/tools/integration-user/README.md b/tools/integration-user/README.md new file mode 100644 index 000000000..73aa37ee2 --- /dev/null +++ b/tools/integration-user/README.md @@ -0,0 +1,21 @@ +# Integration API + +## Add integration user + +To add integration user run: + +`sh npm run integration:add` + +to generate a integration token run: + +`sh npm run integration:token` + +Use generated token to update `access_token` global variable for the provided +Postman collection. Also make sure that the `base_url` variable is properly set. + +## External access management + +To disable the ability for users to create repositories set +`EXTERNAL_ACCESS_MANAGEMENT` .env variable to true. This will lock +repository catalog UI access, remove the ability to clone repositories, and +remove repository based user management UI. diff --git a/tools/integration_user_API.postman_collection.json b/tools/integration-user/integration_user_API.postman_collection.json similarity index 100% rename from tools/integration_user_API.postman_collection.json rename to tools/integration-user/integration_user_API.postman_collection.json From 43086165be95c71f9cf20a89ad7aec4f6e88dd33 Mon Sep 17 00:00:00 2001 From: underscope Date: Fri, 1 Dec 2023 11:00:32 +0100 Subject: [PATCH 12/40] =?UTF-8?q?=F0=9F=92=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/integration-user/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/integration-user/README.md b/tools/integration-user/README.md index 73aa37ee2..bc05664d4 100644 --- a/tools/integration-user/README.md +++ b/tools/integration-user/README.md @@ -4,11 +4,11 @@ To add integration user run: -`sh npm run integration:add` +```sh npm run integration:add``` to generate a integration token run: -`sh npm run integration:token` +```sh npm run integration:token``` Use generated token to update `access_token` global variable for the provided Postman collection. Also make sure that the `base_url` variable is properly set. From 5ad13a6f5a50897f2b6fb202d9f24902ef20bf30 Mon Sep 17 00:00:00 2001 From: underscope Date: Fri, 1 Dec 2023 11:02:51 +0100 Subject: [PATCH 13/40] =?UTF-8?q?=F0=9F=92=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/integration-user/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/integration-user/README.md b/tools/integration-user/README.md index bc05664d4..108207068 100644 --- a/tools/integration-user/README.md +++ b/tools/integration-user/README.md @@ -4,11 +4,15 @@ To add integration user run: -```sh npm run integration:add``` +```shell +npm run integration:add +``` to generate a integration token run: -```sh npm run integration:token``` +```shell +npm run integration:token +``` Use generated token to update `access_token` global variable for the provided Postman collection. Also make sure that the `base_url` variable is properly set. From 80a2693519ae42dfbcf0341c9de8c32729b6c076 Mon Sep 17 00:00:00 2001 From: underscope Date: Sat, 2 Dec 2023 22:14:51 +0100 Subject: [PATCH 14/40] Add UserTag model --- .../20210204115516-create-user-tag.js | 28 +++++++++++++ server/user/userTag.model.js | 40 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 server/shared/database/migrations/20210204115516-create-user-tag.js create mode 100644 server/user/userTag.model.js diff --git a/server/shared/database/migrations/20210204115516-create-user-tag.js b/server/shared/database/migrations/20210204115516-create-user-tag.js new file mode 100644 index 000000000..bb9bd708f --- /dev/null +++ b/server/shared/database/migrations/20210204115516-create-user-tag.js @@ -0,0 +1,28 @@ +'use strict'; + +const TABLE_NAME = 'user_tag'; + +exports.up = (queryInterface, Sequelize) => { + return queryInterface.createTable(TABLE_NAME, { + tagId: { + type: Sequelize.INTEGER, + field: 'tag_id', + references: { model: 'tag', key: 'id' }, + onDelete: 'CASCADE' + }, + userId: { + type: Sequelize.INTEGER, + field: 'user_id', + references: { model: 'user', key: 'id' }, + onDelete: 'CASCADE' + } + }).then(async () => { + return queryInterface.addConstraint(TABLE_NAME, { + name: 'user_tag_pkey', + type: 'primary key', + fields: ['user_id', 'tag_id'] + }); + }); +}; + +exports.down = queryInterface => queryInterface.dropTable(TABLE_NAME); diff --git a/server/user/userTag.model.js b/server/user/userTag.model.js new file mode 100644 index 000000000..0bc730d3f --- /dev/null +++ b/server/user/userTag.model.js @@ -0,0 +1,40 @@ +import { Model } from 'sequelize'; + +class UserTag extends Model { + static fields({ INTEGER }) { + return { + userId: { + type: INTEGER, + field: 'user_id', + primaryKey: true, + unique: 'user_tag_pkey' + }, + tagId: { + type: INTEGER, + field: 'tag_id', + primaryKey: true, + unique: 'user_tag_pkey' + } + }; + } + + static associate({ User, Tag }) { + this.belongsTo(User, { + foreignKey: { name: 'userId', field: 'user_id' } + }); + this.belongsTo(Tag, { + foreignKey: { name: 'tagId', field: 'tag_id' } + }); + } + + static options() { + return { + modelName: 'UserTag', + tableName: 'user_tag', + underscored: true, + timestamps: false + }; + } +} + +export default UserTag; From e3c74e8d8f1cb09827c14635e49fbc20474ce7ef Mon Sep 17 00:00:00 2001 From: underscope Date: Sun, 3 Dec 2023 21:42:31 +0100 Subject: [PATCH 15/40] Add UserTag model associations --- server/tag/tag.model.js | 16 +++++++++++++--- server/user/user.model.js | 21 ++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/server/tag/tag.model.js b/server/tag/tag.model.js index b2997ff28..082558f12 100644 --- a/server/tag/tag.model.js +++ b/server/tag/tag.model.js @@ -18,11 +18,15 @@ class Tag extends Model { }; } - static associate({ Repository, RepositoryTag }) { + static associate({ Repository, RepositoryTag, User, UserTag }) { this.belongsToMany(Repository, { through: RepositoryTag, foreignKey: { name: 'tagId', field: 'tag_id' } }); + this.belongsToMany(User, { + through: UserTag, + foreignKey: { name: 'tagId', field: 'tag_id' } + }); } static getAssociated(user) { @@ -36,9 +40,15 @@ class Tag extends Model { required: true }; if (user && !user.isAdmin()) { - includeRepository.include = [{ model: User, attributes: ['id'], where: { id: user.id } }]; + includeRepository.include = [{ + model: User, + attributes: ['id'], + where: { id: user.id } + }]; } - return Tag.findAll({ include: [includeRepository] }); + return Tag.findAll({ + include: [includeRepository] + }); } static options() { diff --git a/server/user/user.model.js b/server/user/user.model.js index a5f6fc23f..629242c64 100644 --- a/server/user/user.model.js +++ b/server/user/user.model.js @@ -95,7 +95,14 @@ class User extends Model { }; } - static associate({ ActivityStatus, Comment, Repository, RepositoryUser }) { + static associate({ + ActivityStatus, + Comment, + Repository, + RepositoryUser, + Tag, + UserTag + }) { this.hasMany(Comment, { foreignKey: { name: 'authorId', field: 'author_id' } }); @@ -103,6 +110,14 @@ class User extends Model { through: RepositoryUser, foreignKey: { name: 'userId', field: 'user_id' } }); + this.belongsToMany(Tag, { + through: UserTag, + foreignKey: { name: 'userId', field: 'user_id' } + }); + this.hasMany(UserTag, { + as: 'userTags', + foreignKey: { name: 'userId', field: 'user_id' } + }); this.hasMany(ActivityStatus, { as: 'assignedActivities', foreignKey: { name: 'assigneeId', field: 'assignee_id' } @@ -214,6 +229,10 @@ class User extends Model { if (audience === Audience.Scope.Access) return secret; return [secret, this.password, this.createdAt.getTime()].join(''); } + + isAssociatedWithSomeTag(tagIds) { + return this.userTags.some(({ tagId }) => tagIds.includes(tagId)); + } } export default User; From dd93d0c04e8dac5769508f73ab5174d45a6b4dfd Mon Sep 17 00:00:00 2001 From: underscope Date: Sun, 3 Dec 2023 21:53:11 +0100 Subject: [PATCH 16/40] Update association name --- server/repository/repository.model.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/repository/repository.model.js b/server/repository/repository.model.js index c46d3f5ed..3f6006a2c 100644 --- a/server/repository/repository.model.js +++ b/server/repository/repository.model.js @@ -70,6 +70,7 @@ class Repository extends Model { foreignKey: { name: 'repositoryId', field: 'repository_id' } }); this.hasMany(RepositoryTag, { + as: 'repositoryTags', foreignKey: { name: 'repositoryId', field: 'repository_id' } }); this.belongsToMany(Tag, { From 6ad17833dc9220b93af774b0649ffdec98e66734 Mon Sep 17 00:00:00 2001 From: underscope Date: Mon, 4 Dec 2023 16:26:27 +0100 Subject: [PATCH 17/40] Expose user tag API --- server/user/index.js | 4 +++- server/user/user.controller.js | 21 +++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/server/user/index.js b/server/user/index.js index 11c2ac518..9da23a98e 100644 --- a/server/user/index.js +++ b/server/user/index.js @@ -46,7 +46,9 @@ router .patch('/me', ctrl.updateProfile) .post('/me/change-password', ctrl.changePassword) .delete('/:id', authorizeAdminUser, ctrl.remove) - .post('/:id/reinvite', authorizeAdminUser, ctrl.reinvite); + .post('/:id/reinvite', authorizeAdminUser, ctrl.reinvite) + .post('/:id/tag', authorizeAdminUser, ctrl.addTag) + .delete('/:id/tag/:tagId', authorizeAdminUser, ctrl.removeTag); export default { path: '/users', diff --git a/server/user/user.controller.js b/server/user/user.controller.js index 1af261c1b..3fc3d2a93 100644 --- a/server/user/user.controller.js +++ b/server/user/user.controller.js @@ -4,7 +4,7 @@ import db from '../shared/database/index.js'; import map from 'lodash/map.js'; import { Op } from 'sequelize'; -const { User } = db; +const { sequelize, Tag, User, UserTag } = db; const createFilter = q => map(['email', 'firstName', 'lastName'], it => ({ [it]: { [Op.iLike]: `%${q}%` } })); @@ -69,6 +69,21 @@ function reinvite({ params }, res) { .then(() => res.status(ACCEPTED).end()); } +function addTag({ body: { name }, params: { id: userId } }, res) { + return sequelize.transaction(async transaction => { + const user = await User.findByPk(userId, { transaction }); + const [tag] = await Tag.findOrCreate({ where: { name }, transaction }); + await user.addTags([tag], { transaction }); + return res.json({ data: tag }); + }); +} + +async function removeTag({ params: { tagId, id: userId } }, res) { + const where = { tagId, userId }; + await UserTag.destroy({ where }); + return res.status(NO_CONTENT).send(); +} + export default { list, upsert, @@ -78,5 +93,7 @@ export default { getProfile, updateProfile, changePassword, - reinvite + reinvite, + addTag, + removeTag }; From c17f5f04b73116ba6a0cc443b2bf46491d034f5f Mon Sep 17 00:00:00 2001 From: underscope Date: Mon, 4 Dec 2023 16:26:56 +0100 Subject: [PATCH 18/40] Register UserTag model --- server/shared/database/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/shared/database/index.js b/server/shared/database/index.js index f12650a24..946ecb96a 100644 --- a/server/shared/database/index.js +++ b/server/shared/database/index.js @@ -16,6 +16,7 @@ import { wrapMethods } from './helpers.js'; // Require models. /* eslint-disable */ import User from '../../user/user.model.js'; +import UserTag from '../../user/userTag.model.js'; import Repository from '../../repository/repository.model.js'; import RepositoryTag from '../../tag/repositoryTag.model.js'; import RepositoryUser from '../../repository/repositoryUser.model.js'; @@ -79,6 +80,7 @@ function initialize() { */ const models = { User: defineModel(User), + UserTag: defineModel(UserTag), Repository: defineModel(Repository), RepositoryTag: defineModel(RepositoryTag), RepositoryUser: defineModel(RepositoryUser), From 23b85c4f4618e8a8b96e2cfcdc44485b45e48b2a Mon Sep 17 00:00:00 2001 From: underscope Date: Mon, 4 Dec 2023 16:27:24 +0100 Subject: [PATCH 19/40] Add tag based access check --- server/repository/index.js | 5 ++++- server/shared/auth/index.js | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/server/repository/index.js b/server/repository/index.js index 3e28c8e67..8f603d21e 100644 --- a/server/repository/index.js +++ b/server/repository/index.js @@ -77,7 +77,8 @@ function mount(router, mountPath, subrouter) { } function getRepository(req, _res, next, repositoryId) { - return Repository.findByPk(repositoryId, { paranoid: false }) + return Repository + .findByPk(repositoryId, { include: ['repositoryTags'], paranoid: false }) .then(repository => repository || createError(NOT_FOUND, 'Repository not found')) .then(repository => { req.repository = repository; @@ -88,6 +89,8 @@ function getRepository(req, _res, next, repositoryId) { function hasAccess(req, _res, next) { const { user, repository } = req; if (user.isAdmin()) return next(); + const repositoryTagIds = repository.repositoryTags?.map(it => it.tagId); + if (user.isAssociatedWithSomeTag(repositoryTagIds)) return next(); return repository.getUser(user) .then(user => user || createError(UNAUTHORIZED, 'Access restricted')) .then(user => { diff --git a/server/shared/auth/index.js b/server/shared/auth/index.js index 599bfc993..411948e23 100644 --- a/server/shared/auth/index.js +++ b/server/shared/auth/index.js @@ -16,7 +16,9 @@ const options = { }; auth.use(new LocalStrategy(options, (email, password, done) => { - return User.unscoped().findOne({ where: { email } }) + return User + .unscoped() + .findOne({ where: { email } }) .then(user => user && user.authenticate(password)) .then(user => done(null, user || false)) .error(err => done(err, false)); @@ -51,7 +53,7 @@ auth.deserializeUser((user, done) => done(null, user)); export default auth; function verifyJWT(payload, done) { - return User.unscoped().findByPk(payload.id) + return User.unscoped().findByPk(payload.id, { include: ['userTags'] }) .then(user => done(null, user || false)) .error(err => done(err, false)); } From df645aa738d8293c8b22bc23b2f3cc87ebe73dee Mon Sep 17 00:00:00 2001 From: underscope Date: Tue, 5 Dec 2023 09:29:56 +0100 Subject: [PATCH 20/40] Add access tag flag --- .../migrations/20210204115517-add-access-flag-to-tag.js | 9 +++++++++ server/tag/tag.model.js | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 server/shared/database/migrations/20210204115517-add-access-flag-to-tag.js diff --git a/server/shared/database/migrations/20210204115517-add-access-flag-to-tag.js b/server/shared/database/migrations/20210204115517-add-access-flag-to-tag.js new file mode 100644 index 000000000..ba03925b4 --- /dev/null +++ b/server/shared/database/migrations/20210204115517-add-access-flag-to-tag.js @@ -0,0 +1,9 @@ +'use strict'; + +const TABLE_NAME = 'tag'; +const COLUMN_NAME = 'is_access_tag'; + +exports.up = (qi, { BOOLEAN }) => + qi.addColumn(TABLE_NAME, COLUMN_NAME, { type: BOOLEAN, defaultValue: false }); + +exports.down = qi => qi.removeColumn(TABLE_NAME, COLUMN_NAME); diff --git a/server/tag/tag.model.js b/server/tag/tag.model.js index 082558f12..6340cd13a 100644 --- a/server/tag/tag.model.js +++ b/server/tag/tag.model.js @@ -1,7 +1,7 @@ import { Model } from 'sequelize'; class Tag extends Model { - static fields({ STRING, UUID, UUIDV4 }) { + static fields({ BOOLEAN, STRING, UUID, UUIDV4 }) { return { uid: { type: UUID, @@ -14,6 +14,11 @@ class Tag extends Model { allowNull: false, unique: true, validate: { notEmpty: true, len: [2, 20] } + }, + isAccessTag: { + type: BOOLEAN, + field: 'is_access_tag', + defaultValue: false } }; } From 4473109ea649c6c06b76dc77cd367b31f8605d3a Mon Sep 17 00:00:00 2001 From: underscope Date: Tue, 5 Dec 2023 11:34:35 +0100 Subject: [PATCH 21/40] Enable access tag creation only by the integration user --- server/repository/repository.controller.js | 21 ++++++++++++++----- server/tag/tag.model.js | 11 ++++++++++ server/user/user.controller.js | 24 ++++++++++++++++------ 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/server/repository/repository.controller.js b/server/repository/repository.controller.js index 8a40899bf..077dcd559 100644 --- a/server/repository/repository.controller.js +++ b/server/repository/repository.controller.js @@ -1,6 +1,7 @@ import * as fs from 'node:fs'; import * as fsp from 'node:fs/promises'; -import { NO_CONTENT, NOT_FOUND } from 'http-status-codes'; +import { FORBIDDEN, NO_CONTENT, NOT_FOUND } from 'http-status-codes'; +import { repository as role, user as userRole } from '../../config/shared/role.js'; import { createError } from '../shared/error/helpers.js'; import db from '../shared/database/index.js'; import getVal from 'lodash/get.js'; @@ -9,7 +10,6 @@ import { Op } from 'sequelize'; import pick from 'lodash/pick.js'; import Promise from 'bluebird'; import publishingService from '../shared/publishing/publishing.service.js'; -import { repository as role } from '../../config/shared/role.js'; import sample from 'lodash/sample.js'; import { schema } from '../../config/shared/tailor.loader.js'; import { snakeCase } from 'change-case'; @@ -171,15 +171,26 @@ function findOrCreateRole(repository, user, role) { .then(() => user); } -function addTag({ body: { name }, repository }, res) { +function addTag( + { + body: { name, isAccessTag = false }, + user, + repository + }, + res +) { return sequelize.transaction(async transaction => { - const [tag] = await Tag.findOrCreate({ where: { name }, transaction }); + const tag = await Tag.fetchOrCreate({ user, name, isAccessTag, transaction }); await repository.addTags([tag], { transaction }); return res.json({ data: tag }); }); } -async function removeTag({ params: { tagId, repositoryId } }, res) { +async function removeTag({ user, params: { tagId, repositoryId } }, res) { + const tag = await Tag.findByPk(tagId); + if (tag.isAccessTag && user.role !== userRole.INTEGRATION) { + return res.status(FORBIDDEN); + } const where = { tagId, repositoryId }; await RepositoryTag.destroy({ where }); return res.status(NO_CONTENT).send(); diff --git a/server/tag/tag.model.js b/server/tag/tag.model.js index 6340cd13a..067458eb6 100644 --- a/server/tag/tag.model.js +++ b/server/tag/tag.model.js @@ -1,4 +1,5 @@ import { Model } from 'sequelize'; +import { user as userRole } from '../../config/shared/role.js'; class Tag extends Model { static fields({ BOOLEAN, STRING, UUID, UUIDV4 }) { @@ -34,6 +35,16 @@ class Tag extends Model { }); } + static async fetchOrCreate({ user, name, isAccessTag = false, transaction }) { + if (isAccessTag && user.role !== userRole.INTEGRATION) { + throw new Error('Only integration user can create access tags'); + } + const tag = await Tag.findOne({ where: { name }, transaction }); + if (!tag) return this.create({ name, isAccessTag }, { transaction }); + if (tag && tag.isAccessTag === isAccessTag) return tag; + throw new Error('Cannot change tag type'); + } + static getAssociated(user) { const Repository = this.sequelize.model('Repository'); const User = this.sequelize.model('User'); diff --git a/server/user/user.controller.js b/server/user/user.controller.js index 3fc3d2a93..4c7844622 100644 --- a/server/user/user.controller.js +++ b/server/user/user.controller.js @@ -1,8 +1,9 @@ -import { ACCEPTED, BAD_REQUEST, CONFLICT, NO_CONTENT, NOT_FOUND } from 'http-status-codes'; +import { ACCEPTED, BAD_REQUEST, CONFLICT, FORBIDDEN, NO_CONTENT, NOT_FOUND } from 'http-status-codes'; import { createError, validationError } from '../shared/error/helpers.js'; import db from '../shared/database/index.js'; import map from 'lodash/map.js'; import { Op } from 'sequelize'; +import { user as userRole } from '../../config/shared/role.js'; const { sequelize, Tag, User, UserTag } = db; const createFilter = q => map(['email', 'firstName', 'lastName'], @@ -69,16 +70,27 @@ function reinvite({ params }, res) { .then(() => res.status(ACCEPTED).end()); } -function addTag({ body: { name }, params: { id: userId } }, res) { +function addTag( + { + user, + body: { name, isAccessTag }, + params: { id: userId } + }, + res +) { return sequelize.transaction(async transaction => { - const user = await User.findByPk(userId, { transaction }); - const [tag] = await Tag.findOrCreate({ where: { name }, transaction }); - await user.addTags([tag], { transaction }); + const tag = await Tag.fetchOrCreate({ user, name, isAccessTag, transaction }); + const tagUser = await User.findByPk(userId, { transaction }); + await tagUser.addTags([tag], { transaction }); return res.json({ data: tag }); }); } -async function removeTag({ params: { tagId, id: userId } }, res) { +async function removeTag({ user, params: { tagId, id: userId } }, res) { + const tag = await Tag.findByPk(tagId); + if (tag.isAccessTag && user.role !== userRole.INTEGRATION) { + return res.status(FORBIDDEN); + } const where = { tagId, userId }; await UserTag.destroy({ where }); return res.status(NO_CONTENT).send(); From 1a0e88dae7e46b3f633f5cae61830a0dcd603edc Mon Sep 17 00:00:00 2001 From: underscope Date: Tue, 5 Dec 2023 12:00:51 +0100 Subject: [PATCH 22/40] Disable access tag deletion --- client/components/catalog/Card/Tags/index.vue | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/components/catalog/Card/Tags/index.vue b/client/components/catalog/Card/Tags/index.vue index 700f3d694..b574b91e8 100644 --- a/client/components/catalog/Card/Tags/index.vue +++ b/client/components/catalog/Card/Tags/index.vue @@ -2,19 +2,23 @@
{{ name }} From 2132c7a8d09989b47167bb38e30b0e24aed72cf1 Mon Sep 17 00:00:00 2001 From: underscope Date: Tue, 5 Dec 2023 18:13:33 +0100 Subject: [PATCH 23/40] Update tag based access check --- server/repository/index.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/server/repository/index.js b/server/repository/index.js index 8f603d21e..851a65eb0 100644 --- a/server/repository/index.js +++ b/server/repository/index.js @@ -22,7 +22,7 @@ const { user: role } = roleConfig; import storageRouter from '../shared/storage/storage.router.js'; /* eslint-enable */ -const { Repository } = db; +const { Repository, Tag } = db; const router = express.Router(); const { setSignedCookies } = proxyMw(storage, proxy); @@ -42,7 +42,7 @@ router router .param('repositoryId', getRepository) - .use('/:repositoryId', hasAccess, setSignedCookies); + .use('/:repositoryId', hasAccess); router.route('/') .get(processQuery({ limit: 100 }), ctrl.index) @@ -57,7 +57,7 @@ router .post('/:repositoryId/pin', ctrl.pin) .post('/:repositoryId/clone', authorizeAdminUser, ctrl.clone) .post('/:repositoryId/publish', ctrl.publishRepoInfo) - .get('/:repositoryId/users', authorizeAdminUser, ctrl.getUsers) + .get('/:repositoryId/users', ctrl.getUsers) .get('/:repositoryId/export/setup', ctrl.initiateExportJob) .post('/:repositoryId/export/:jobId', ctrl.export) .post('/:repositoryId/users', authorizeAdminUser, ctrl.upsertUser) @@ -78,7 +78,7 @@ function mount(router, mountPath, subrouter) { function getRepository(req, _res, next, repositoryId) { return Repository - .findByPk(repositoryId, { include: ['repositoryTags'], paranoid: false }) + .findByPk(repositoryId, { include: [{ model: Tag }], paranoid: false }) .then(repository => repository || createError(NOT_FOUND, 'Repository not found')) .then(repository => { req.repository = repository; @@ -89,8 +89,13 @@ function getRepository(req, _res, next, repositoryId) { function hasAccess(req, _res, next) { const { user, repository } = req; if (user.isAdmin()) return next(); - const repositoryTagIds = repository.repositoryTags?.map(it => it.tagId); - if (user.isAssociatedWithSomeTag(repositoryTagIds)) return next(); + const repositoryTagIds = repository.tags + ?.filter(it => it.isAccessTag) + .map(it => it.id); + if (repositoryTagIds.length && user.isAssociatedWithSomeTag(repositoryTagIds)) { + req.repositoryRole = role.ADMIN; + return next(); + } return repository.getUser(user) .then(user => user || createError(UNAUTHORIZED, 'Access restricted')) .then(user => { From 885b9a317ce0ab2163e49c66edc107a77d43e354 Mon Sep 17 00:00:00 2001 From: underscope Date: Wed, 6 Dec 2023 09:36:11 +0100 Subject: [PATCH 24/40] =?UTF-8?q?=F0=9F=92=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/repository/repository.model.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/repository/repository.model.js b/server/repository/repository.model.js index 3f6006a2c..c46d3f5ed 100644 --- a/server/repository/repository.model.js +++ b/server/repository/repository.model.js @@ -70,7 +70,6 @@ class Repository extends Model { foreignKey: { name: 'repositoryId', field: 'repository_id' } }); this.hasMany(RepositoryTag, { - as: 'repositoryTags', foreignKey: { name: 'repositoryId', field: 'repository_id' } }); this.belongsToMany(Tag, { From 1cf83ce0c5e8bd964e53231d090e57fad11a6050 Mon Sep 17 00:00:00 2001 From: underscope Date: Wed, 6 Dec 2023 09:47:57 +0100 Subject: [PATCH 25/40] throw error in case of missing resource --- server/user/user.controller.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/user/user.controller.js b/server/user/user.controller.js index 4c7844622..7a8b738db 100644 --- a/server/user/user.controller.js +++ b/server/user/user.controller.js @@ -79,17 +79,21 @@ function addTag( res ) { return sequelize.transaction(async transaction => { - const tag = await Tag.fetchOrCreate({ user, name, isAccessTag, transaction }); const tagUser = await User.findByPk(userId, { transaction }); + if (!tagUser) return createError(NOT_FOUND, 'User not found'); + const tag = await Tag.fetchOrCreate({ user, name, isAccessTag, transaction }); await tagUser.addTags([tag], { transaction }); return res.json({ data: tag }); }); } async function removeTag({ user, params: { tagId, id: userId } }, res) { + const tagUser = await User.findByPk(userId); + if (!tagUser) return createError(NOT_FOUND, 'User not found'); const tag = await Tag.findByPk(tagId); + if (!tag) return createError(NOT_FOUND, 'Tag not found'); if (tag.isAccessTag && user.role !== userRole.INTEGRATION) { - return res.status(FORBIDDEN); + return res.status(FORBIDDEN, 'Only integration users can remove access tags'); } const where = { tagId, userId }; await UserTag.destroy({ where }); From 6239a00b4de645037b104647a52faea9c50495a4 Mon Sep 17 00:00:00 2001 From: underscope Date: Wed, 6 Dec 2023 09:55:49 +0100 Subject: [PATCH 26/40] Improve error handling --- server/repository/repository.controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/repository/repository.controller.js b/server/repository/repository.controller.js index 077dcd559..9ed7cb8bb 100644 --- a/server/repository/repository.controller.js +++ b/server/repository/repository.controller.js @@ -188,8 +188,9 @@ function addTag( async function removeTag({ user, params: { tagId, repositoryId } }, res) { const tag = await Tag.findByPk(tagId); + if (!tag) return createError(NOT_FOUND, 'Tag not found'); if (tag.isAccessTag && user.role !== userRole.INTEGRATION) { - return res.status(FORBIDDEN); + return createError(FORBIDDEN, 'Can be removed only by integration users'); } const where = { tagId, repositoryId }; await RepositoryTag.destroy({ where }); From c60573c1b4a16de825efbaf70a29d36b78fc6b51 Mon Sep 17 00:00:00 2001 From: underscope Date: Wed, 6 Dec 2023 10:26:09 +0100 Subject: [PATCH 27/40] Revert change --- server/repository/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/repository/index.js b/server/repository/index.js index 851a65eb0..47909bd49 100644 --- a/server/repository/index.js +++ b/server/repository/index.js @@ -42,7 +42,7 @@ router router .param('repositoryId', getRepository) - .use('/:repositoryId', hasAccess); + .use('/:repositoryId', hasAccess, setSignedCookies); router.route('/') .get(processQuery({ limit: 100 }), ctrl.index) From 6be83a0f4b1cd4dcb12b0c6a610254d3670a43a7 Mon Sep 17 00:00:00 2001 From: underscope Date: Wed, 6 Dec 2023 10:31:03 +0100 Subject: [PATCH 28/40] =?UTF-8?q?=F0=9F=92=84=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/integration-user/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/integration-user/README.md b/tools/integration-user/README.md index 108207068..73949b822 100644 --- a/tools/integration-user/README.md +++ b/tools/integration-user/README.md @@ -19,7 +19,7 @@ Postman collection. Also make sure that the `base_url` variable is properly set. ## External access management -To disable the ability for users to create repositories set -`EXTERNAL_ACCESS_MANAGEMENT` .env variable to true. This will lock +To disable the ability for users to create repositories and manage access rights +set `EXTERNAL_ACCESS_MANAGEMENT` .env variable to true. This will lock repository catalog UI access, remove the ability to clone repositories, and -remove repository based user management UI. +remove user management UI. From ff99b45f333cb9b54c9b7f2fe5f7737f7ea6f8de Mon Sep 17 00:00:00 2001 From: underscope Date: Thu, 7 Dec 2023 11:13:15 +0100 Subject: [PATCH 29/40] Lock user management page access --- .../components/repository/Settings/UserManagement/index.vue | 4 ++++ client/components/system-settings/Sidebar.vue | 5 ++++- client/components/system-settings/UserManagement/index.vue | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/client/components/repository/Settings/UserManagement/index.vue b/client/components/repository/Settings/UserManagement/index.vue index 41a718d46..ba796a8b6 100644 --- a/client/components/repository/Settings/UserManagement/index.vue +++ b/client/components/repository/Settings/UserManagement/index.vue @@ -18,8 +18,12 @@ import UserList from './UserList.vue'; export default { name: 'repository-user-management', computed: { + isExternalAccessManagement: () => process.env.EXTERNAL_ACCESS_MANAGEMENT, roles: () => map(role.repository, value => ({ text: titleCase(value), value })) }, + created() { + if (this.isExternalAccessManagement) this.$router.go(-1); + }, components: { AddUserDialog, UserList diff --git a/client/components/system-settings/Sidebar.vue b/client/components/system-settings/Sidebar.vue index 4f90ca75d..b2662dd92 100644 --- a/client/components/system-settings/Sidebar.vue +++ b/client/components/system-settings/Sidebar.vue @@ -20,12 +20,15 @@ export default { name: 'system-settings-sidebar', computed: { + isExternalAccessManagement: () => process.env.EXTERNAL_ACCESS_MANAGEMENT, routes() { - return [ + const items = [ { label: 'System Users', name: 'system-user-management', icon: 'account' }, { label: 'Structure Types', name: 'installed-schemas', icon: 'file-tree' }, { label: 'Installed Elements', name: 'installed-elements', icon: 'puzzle' } ]; + if (this.isExternalAccessManagement) items.shift(); + return items; } } }; diff --git a/client/components/system-settings/UserManagement/index.vue b/client/components/system-settings/UserManagement/index.vue index 5c027305f..750f6408f 100644 --- a/client/components/system-settings/UserManagement/index.vue +++ b/client/components/system-settings/UserManagement/index.vue @@ -123,6 +123,7 @@ export default { }, computed: { ...mapState({ user: state => state.auth.user }), + isExternalAccessManagement: () => process.env.EXTERNAL_ACCESS_MANAGEMENT, headers, defaultPage }, @@ -166,6 +167,9 @@ export default { this.fetch(); } }, + created() { + if (this.isExternalAccessManagement) this.$router.go(-1); + }, components: { UserDialog } }; From 04773c55db5a6ccc736742892f3d239e1a71365e Mon Sep 17 00:00:00 2001 From: underscope Date: Fri, 8 Dec 2023 16:09:57 +0100 Subject: [PATCH 30/40] Add tag CRUD --- server/tag/index.js | 31 ++++++++++++++++++++++++++++++- server/tag/tag.controller.js | 32 +++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/server/tag/index.js b/server/tag/index.js index 90257d38c..2fe00b893 100644 --- a/server/tag/index.js +++ b/server/tag/index.js @@ -1,10 +1,39 @@ +import { authorize } from '../shared/auth/mw.js'; +import { createError } from '../shared/error/helpers.js'; import ctrl from './tag.controller.js'; +import db from '../shared/database/index.js'; import express from 'express'; +import { NOT_FOUND } from 'http-status-codes'; +import roleConfig from '../../config/shared/role.js'; const router = express.Router(); +const { Tag } = db; +const { user: role } = roleConfig; + +const authorizeUser = authorize(role.INTEGRATION); + +router + .get('/', ctrl.list) + .post('/', authorizeUser, ctrl.create); router - .get('/', ctrl.list); + .param('tagId', getTag) + .use('/:tagId', authorizeUser); + +router.route('/:tagId') + .get(ctrl.get) + .patch(ctrl.patch) + .delete(ctrl.remove); + +function getTag(req, _res, next, tagId) { + return Tag + .findByPk(tagId, { paranoid: false }) + .then(tag => tag || createError(NOT_FOUND, 'Tag not found')) + .then(tag => { + req.tag = tag; + next(); + }); +} export default { path: '/tags', diff --git a/server/tag/tag.controller.js b/server/tag/tag.controller.js index 9b93f3d93..843908d21 100644 --- a/server/tag/tag.controller.js +++ b/server/tag/tag.controller.js @@ -1,4 +1,6 @@ import db from '../shared/database/index.js'; +import { NO_CONTENT } from 'http-status-codes'; +import pick from 'lodash/pick.js'; import yn from 'yn'; const { Tag } = db; @@ -10,6 +12,34 @@ async function list({ user, query: { associated } }, res) { return res.json({ data: tags }); } +async function get({ tag }, res) { + return res.json({ data: tag }); +} + +function create({ body }, res) { + const attrs = ['name', 'isAccessTag']; + const payload = pick(body, attrs); + return Tag.create(payload) + .then(data => res.json({ data })); +} + +function patch({ tag, body }, res) { + const attrs = ['name', 'isAccessTag']; + const payload = pick(body, attrs); + return tag.update(payload) + .then(tag => tag.reload()) + .then(data => res.json({ data })); +} + +function remove({ tag }, res) { + return tag.destroy() + .then(data => res.status(NO_CONTENT).end()); +} + export default { - list + list, + get, + create, + patch, + remove }; From 70dced58e3e5e90d709314a519d0fa9fd66ad09a Mon Sep 17 00:00:00 2001 From: underscope Date: Mon, 11 Dec 2023 07:21:09 +0100 Subject: [PATCH 31/40] =?UTF-8?q?=F0=9F=92=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/tag/tag.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/tag/tag.controller.js b/server/tag/tag.controller.js index 843908d21..dac2d10a6 100644 --- a/server/tag/tag.controller.js +++ b/server/tag/tag.controller.js @@ -33,7 +33,7 @@ function patch({ tag, body }, res) { function remove({ tag }, res) { return tag.destroy() - .then(data => res.status(NO_CONTENT).end()); + .then(() => res.status(NO_CONTENT).end()); } export default { From 0d48d482ea741d7f6fedf9b48bd9bbdc5d6d21f4 Mon Sep 17 00:00:00 2001 From: underscope Date: Mon, 11 Dec 2023 07:24:34 +0100 Subject: [PATCH 32/40] Update postman collection --- ...tegration_user_API.postman_collection.json | 1631 ++++++++++------- 1 file changed, 961 insertions(+), 670 deletions(-) diff --git a/tools/integration-user/integration_user_API.postman_collection.json b/tools/integration-user/integration_user_API.postman_collection.json index 65484a37a..0db703761 100644 --- a/tools/integration-user/integration_user_API.postman_collection.json +++ b/tools/integration-user/integration_user_API.postman_collection.json @@ -1,671 +1,962 @@ { - "info": { - "_postman_id": "d2543cae-4168-45ba-bb48-3d4efc4f0d9f", - "name": "Tailor integration user API", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "31477572" - }, - "item": [ - { - "name": "repository", - "item": [ - { - "name": "user", - "item": [ - { - "name": "Get repository users", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/repositories/:id/users", - "host": ["{{base_url}}"], - "path": ["repositories", ":id", "users"], - "variable": [ - { - "key": "id", - "value": "1" - } - ] - }, - "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." - }, - "response": [] - }, - { - "name": "Enable repository access", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"admin@example.com\",\n \"role\": \"ADMIN\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/repositories/:id/users", - "host": ["{{base_url}}"], - "path": ["repositories", ":id", "users"], - "variable": [ - { - "key": "id", - "value": "1" - } - ] - }, - "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." - }, - "response": [] - }, - { - "name": "Revoke repository access", - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [], - "url": { - "raw": "{{base_url}}/repositories/:id/users/:userId", - "host": ["{{base_url}}"], - "path": ["repositories", ":id", "users", ":userId"], - "variable": [ - { - "key": "id", - "value": "1" - }, - { - "key": "userId", - "value": "1" - } - ] - } - }, - "response": [] - } - ] - }, - { - "name": "tag", - "item": [ - { - "name": "Add tag", - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Test tag\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/repositories/:id/tags", - "host": ["{{base_url}}"], - "path": ["repositories", ":id", "tags"], - "variable": [ - { - "key": "id", - "value": "1" - } - ] - } - }, - "response": [] - }, - { - "name": "Remove tag", - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "{{base_url}}/repositories/:id/tags/:tagId", - "host": ["{{base_url}}"], - "path": ["repositories", ":id", "tags", ":tagId"], - "variable": [ - { - "key": "id", - "value": "" - }, - { - "key": "tagId", - "value": "" - } - ] - } - }, - "response": [] - } - ] - }, - { - "name": "List repositories", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/info?id=1", - "host": ["{{base_url}}"], - "path": ["info"], - "query": [ - { - "key": "id", - "value": "1" - } - ] - }, - "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." - }, - "response": [] - }, - { - "name": "List repositories with filter and pagination", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/repositories?search=te&offset=0&limit=21&sortOrder=DESC&sortBy=createdAt&tagIds[]=1", - "host": ["{{base_url}}"], - "path": ["repositories"], - "query": [ - { - "key": "search", - "value": "te" - }, - { - "key": "offset", - "value": "0" - }, - { - "key": "limit", - "value": "21" - }, - { - "key": "sortOrder", - "value": "DESC" - }, - { - "key": "sortBy", - "value": "createdAt" - }, - { - "key": "tagIds[]", - "value": "1" - } - ] - }, - "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." - }, - "response": [] - }, - { - "name": "Get repository", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/info?id=1", - "host": ["{{base_url}}"], - "path": ["info"], - "query": [ - { - "key": "id", - "value": "1" - } - ] - }, - "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." - }, - "response": [] - }, - { - "name": "Create repository", - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Test\",\n \"description\": \"Test\",\n \"schema\": \"TEST_SCHEMA\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/repositories", - "host": ["{{base_url}}"], - "path": ["repositories"] - } - }, - "response": [] - }, - { - "name": "Clone repository", - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Test clone\",\n \"description\": \"Test clone\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/repositories/:id/clone", - "host": ["{{base_url}}"], - "path": ["repositories", ":id", "clone"], - "variable": [ - { - "key": "id", - "value": "1" - } - ] - } - }, - "response": [] - }, - { - "name": "Import repository", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "archive", - "type": "file", - "src": "/Users/underscope/Desktop/test123.tgz" - }, - { - "key": "name", - "value": "Test import", - "type": "text" - }, - { - "key": "description", - "value": "Test import", - "type": "text" - } - ] - }, - "url": { - "raw": "{{base_url}}/repositories/import", - "host": ["{{base_url}}"], - "path": ["repositories", "import"] - } - }, - "response": [] - } - ] - }, - { - "name": "tag", - "item": [ - { - "name": "List", - "request": { - "method": "GET", - "header": [] - }, - "response": [] - } - ] - }, - { - "name": "user", - "item": [ - { - "name": "List", - "request": { - "method": "GET", - "header": [] - }, - "response": [] - }, - { - "name": "Upsert", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"test@gostudion.com\",\n \"firstName\": \"test\",\n \"lastName\": \"test\",\n \"role\": \"USER\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/users", - "host": ["{{base_url}}"], - "path": ["users"] - } - }, - "response": [] - }, - { - "name": "Delete", - "request": { - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"test@gostudion.com\",\n \"firstName\": \"test\",\n \"lastName\": \"test\",\n \"role\": \"USER\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/users/:id", - "host": ["{{base_url}}"], - "path": ["users", ":id"], - "variable": [ - { - "key": "id", - "value": "1" - } - ] - } - }, - "response": [] - }, - { - "name": "Reinvite", - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"test@gostudion.com\",\n \"firstName\": \"test\",\n \"lastName\": \"test\",\n \"role\": \"USER\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/users/:id/reinvite", - "host": ["{{base_url}}"], - "path": ["users", ":id", "reinvite"], - "variable": [ - { - "key": "id", - "value": "1" - } - ] - } - }, - "response": [] - } - ] - } - ], - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{access_token}}", - "type": "string" - }, - { - "key": "key", - "value": "access_token", - "type": "string" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [""] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [""] - } - } - ] -} + "info": { + "_postman_id": "d2543cae-4168-45ba-bb48-3d4efc4f0d9f", + "name": "Tailor integration user API", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "31477572" + }, + "item": [ + { + "name": "repository", + "item": [ + { + "name": "user", + "item": [ + { + "name": "Get repository users", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/repositories/:id/users", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + ":id", + "users" + ], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Enable repository access", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"admin@example.com\",\n \"role\": \"ADMIN\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/repositories/:id/users", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + ":id", + "users" + ], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Revoke repository access", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base_url}}/repositories/:id/users/:userId", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + ":id", + "users", + ":userId" + ], + "variable": [ + { + "key": "id", + "value": "1" + }, + { + "key": "userId", + "value": "1" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "tag", + "item": [ + { + "name": "Add tag", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test\",\n \"isAccessTag\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/repositories/:id/tags", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + ":id", + "tags" + ], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Remove tag", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base_url}}/repositories/:id/tags/:tagId", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + ":id", + "tags", + ":tagId" + ], + "variable": [ + { + "key": "id", + "value": "" + }, + { + "key": "tagId", + "value": "" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "List repositories", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/info?id=1", + "host": [ + "{{base_url}}" + ], + "path": [ + "info" + ], + "query": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "List repositories with filter and pagination", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/repositories?search=te&offset=0&limit=21&sortOrder=DESC&sortBy=createdAt&tagIds[]=1", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories" + ], + "query": [ + { + "key": "search", + "value": "te" + }, + { + "key": "offset", + "value": "0" + }, + { + "key": "limit", + "value": "21" + }, + { + "key": "sortOrder", + "value": "DESC" + }, + { + "key": "sortBy", + "value": "createdAt" + }, + { + "key": "tagIds[]", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Get repository", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/info?id=1", + "host": [ + "{{base_url}}" + ], + "path": [ + "info" + ], + "query": [ + { + "key": "id", + "value": "1" + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Create repository", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test\",\n \"description\": \"Test\",\n \"schema\": \"TEST_SCHEMA\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/repositories", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories" + ] + } + }, + "response": [] + }, + { + "name": "Clone repository", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test clone\",\n \"description\": \"Test clone\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/repositories/:id/clone", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + ":id", + "clone" + ], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Import repository", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "archive", + "type": "file", + "src": "/Users/underscope/Desktop/test123.tgz" + }, + { + "key": "name", + "value": "Test import", + "type": "text" + }, + { + "key": "description", + "value": "Test import", + "type": "text" + } + ] + }, + "url": { + "raw": "{{base_url}}/repositories/import", + "host": [ + "{{base_url}}" + ], + "path": [ + "repositories", + "import" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "tag", + "item": [ + { + "name": "List", + "request": { + "method": "GET", + "header": [] + }, + "response": [] + }, + { + "name": "Get", + "request": { + "method": "GET", + "header": [] + }, + "response": [] + }, + { + "name": "Create", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test update\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/tags", + "host": [ + "{{base_url}}" + ], + "path": [ + "tags" + ] + } + }, + "response": [] + }, + { + "name": "Update", + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test update\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/tags/:tagId", + "host": [ + "{{base_url}}" + ], + "path": [ + "tags", + ":tagId" + ], + "variable": [ + { + "key": "tagId", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Remove", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test update\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/tags/:tagId", + "host": [ + "{{base_url}}" + ], + "path": [ + "tags", + ":tagId" + ], + "variable": [ + { + "key": "tagId", + "value": "1" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "user", + "item": [ + { + "name": "List", + "request": { + "method": "GET", + "header": [] + }, + "response": [] + }, + { + "name": "Upsert", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"test@gostudion.com\",\n \"firstName\": \"test\",\n \"lastName\": \"test\",\n \"role\": \"USER\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/users", + "host": [ + "{{base_url}}" + ], + "path": [ + "users" + ] + } + }, + "response": [] + }, + { + "name": "Delete", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"test@gostudion.com\",\n \"firstName\": \"test\",\n \"lastName\": \"test\",\n \"role\": \"USER\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/users/:id", + "host": [ + "{{base_url}}" + ], + "path": [ + "users", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Reinvite", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"test@gostudion.com\",\n \"firstName\": \"test\",\n \"lastName\": \"test\",\n \"role\": \"USER\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/users/:id/reinvite", + "host": [ + "{{base_url}}" + ], + "path": [ + "users", + ":id", + "reinvite" + ], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Tag user", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test\",\n \"isAccessTag\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/users/:id/tag", + "host": [ + "{{base_url}}" + ], + "path": [ + "users", + ":id", + "tag" + ], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Remove tag", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"test@gostudion.com\",\n \"firstName\": \"test\",\n \"lastName\": \"test\",\n \"role\": \"USER\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/users/:id/tag/:tagId", + "host": [ + "{{base_url}}" + ], + "path": [ + "users", + ":id", + "tag", + ":tagId" + ], + "variable": [ + { + "key": "id", + "value": "1" + }, + { + "key": "tagId", + "value": "4" + } + ] + } + }, + "response": [] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{access_token}}", + "type": "string" + }, + { + "key": "key", + "value": "access_token", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] +} \ No newline at end of file From 2baf054cb5d82123c0ead8f3f7e24b913bae0fa8 Mon Sep 17 00:00:00 2001 From: underscope Date: Mon, 11 Dec 2023 07:35:39 +0100 Subject: [PATCH 33/40] =?UTF-8?q?=F0=9F=92=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/tag/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/tag/index.js b/server/tag/index.js index 2fe00b893..f7413dab0 100644 --- a/server/tag/index.js +++ b/server/tag/index.js @@ -27,7 +27,7 @@ router.route('/:tagId') function getTag(req, _res, next, tagId) { return Tag - .findByPk(tagId, { paranoid: false }) + .findByPk(tagId) .then(tag => tag || createError(NOT_FOUND, 'Tag not found')) .then(tag => { req.tag = tag; From 7cfb7f907afee56717cf546aba88d4a0381764c9 Mon Sep 17 00:00:00 2001 From: underscope Date: Mon, 11 Dec 2023 07:52:48 +0100 Subject: [PATCH 34/40] Fix issue with missing unlink callback --- server/repository/repository.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/repository/repository.controller.js b/server/repository/repository.controller.js index 9ed7cb8bb..8fa40d794 100644 --- a/server/repository/repository.controller.js +++ b/server/repository/repository.controller.js @@ -234,8 +234,8 @@ function importRepository({ body, file, user }, res) { return TransferService .createImportJob(path, options) .toPromise() - .finally(() => { - fs.unlink(path); + .finally(async () => { + await fsp.unlink(path); res.end(); }); } From efba78ab3aaed4a545c74db16d0619b6c013726b Mon Sep 17 00:00:00 2001 From: underscope Date: Mon, 11 Dec 2023 09:21:35 +0100 Subject: [PATCH 35/40] Update access rights - If external mgm limit actions to integration user --- server/repository/index.js | 21 +++++++++++---------- server/shared/auth/mw.js | 6 ++++++ server/tag/index.js | 10 +++------- server/user/index.js | 20 +++++++++----------- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/server/repository/index.js b/server/repository/index.js index 47909bd49..260f6fc38 100644 --- a/server/repository/index.js +++ b/server/repository/index.js @@ -1,5 +1,5 @@ +import { authorize, authorizeIntegration } from '../shared/auth/mw.js'; import { NOT_FOUND, UNAUTHORIZED } from 'http-status-codes'; -import { authorize } from '../shared/auth/mw.js'; import { createError } from '../shared/error/helpers.js'; import ctrl from './repository.controller.js'; import db from '../shared/database/index.js'; @@ -30,15 +30,16 @@ const { EXTERNAL_ACCESS_MANAGEMENT: isExternalAccessManagement } = process.env; -const authorizeAdminUser = isExternalAccessManagement - ? authorize(role.INTEGRATION) +const authorizeUser = isExternalAccessManagement + ? authorizeIntegration : authorize(); + // NOTE: disk storage engine expects an object to be passed as the first argument // https://github.com/expressjs/multer/blob/6b5fff5/storage/disk.js#L17-L18 const upload = multer({ storage: multer.diskStorage({}) }); router - .post('/import', authorizeAdminUser, upload.single('archive'), ctrl.import); + .post('/import', authorizeUser, upload.single('archive'), ctrl.import); router .param('repositoryId', getRepository) @@ -46,7 +47,7 @@ router router.route('/') .get(processQuery({ limit: 100 }), ctrl.index) - .post(authorizeAdminUser, ctrl.create); + .post(authorizeUser, ctrl.create); router.route('/:repositoryId') .get(ctrl.get) @@ -55,15 +56,15 @@ router.route('/:repositoryId') router .post('/:repositoryId/pin', ctrl.pin) - .post('/:repositoryId/clone', authorizeAdminUser, ctrl.clone) + .post('/:repositoryId/clone', authorizeUser, ctrl.clone) .post('/:repositoryId/publish', ctrl.publishRepoInfo) .get('/:repositoryId/users', ctrl.getUsers) .get('/:repositoryId/export/setup', ctrl.initiateExportJob) .post('/:repositoryId/export/:jobId', ctrl.export) - .post('/:repositoryId/users', authorizeAdminUser, ctrl.upsertUser) - .delete('/:repositoryId/users/:userId', authorizeAdminUser, ctrl.removeUser) - .post('/:repositoryId/tags', authorizeAdminUser, ctrl.addTag) - .delete('/:repositoryId/tags/:tagId', authorizeAdminUser, ctrl.removeTag); + .post('/:repositoryId/users', authorizeUser, ctrl.upsertUser) + .delete('/:repositoryId/users/:userId', authorizeUser, ctrl.removeUser) + .post('/:repositoryId/tags', authorizeUser, ctrl.addTag) + .delete('/:repositoryId/tags/:tagId', authorizeUser, ctrl.removeTag); mount(router, '/:repositoryId', feed); mount(router, '/:repositoryId', activity); diff --git a/server/shared/auth/mw.js b/server/shared/auth/mw.js index 9e704f90a..de196c23f 100644 --- a/server/shared/auth/mw.js +++ b/server/shared/auth/mw.js @@ -14,6 +14,11 @@ function authorize(...allowed) { }; } +function authorizeIntegration({ user }, res, next) { + if (user && user.role === role.INTEGRATION) return next(); + return createError(UNAUTHORIZED, 'Access restricted'); +} + function extractAuthData(req, res, next) { const path = authConfig.jwt.cookie.signed ? 'signedCookies' : 'cookies'; req.authData = get(req[path], 'auth', null); @@ -22,5 +27,6 @@ function extractAuthData(req, res, next) { export { authorize, + authorizeIntegration, extractAuthData }; diff --git a/server/tag/index.js b/server/tag/index.js index f7413dab0..2133be6a0 100644 --- a/server/tag/index.js +++ b/server/tag/index.js @@ -1,24 +1,20 @@ -import { authorize } from '../shared/auth/mw.js'; +import { authorizeIntegration } from '../shared/auth/mw.js'; import { createError } from '../shared/error/helpers.js'; import ctrl from './tag.controller.js'; import db from '../shared/database/index.js'; import express from 'express'; import { NOT_FOUND } from 'http-status-codes'; -import roleConfig from '../../config/shared/role.js'; const router = express.Router(); const { Tag } = db; -const { user: role } = roleConfig; - -const authorizeUser = authorize(role.INTEGRATION); router .get('/', ctrl.list) - .post('/', authorizeUser, ctrl.create); + .post('/', authorizeIntegration, ctrl.create); router .param('tagId', getTag) - .use('/:tagId', authorizeUser); + .use('/:tagId', authorizeIntegration); router.route('/:tagId') .get(ctrl.get) diff --git a/server/user/index.js b/server/user/index.js index 9da23a98e..7cbcabc1d 100644 --- a/server/user/index.js +++ b/server/user/index.js @@ -1,24 +1,22 @@ +import { authorize, authorizeIntegration } from '../shared/auth/mw.js'; import { loginRequestLimiter, resetLoginAttempts, setLoginLimitKey } from './mw.js'; import { ACCEPTED } from 'http-status-codes'; -import { authorize } from '../shared/auth/mw.js'; import authService from '../shared/auth/index.js'; import ctrl from './user.controller.js'; import db from '../shared/database/index.js'; import express from 'express'; import { processPagination } from '../shared/database/pagination.js'; import { requestLimiter } from '../shared/request/mw.js'; -import roleConfig from '../../config/shared/role.js'; const { User } = db; const router = express.Router(); -const { user: role } = roleConfig; const { EXTERNAL_ACCESS_MANAGEMENT: isExternalAccessManagement } = process.env; -const authorizeAdminUser = isExternalAccessManagement - ? authorize(role.INTEGRATION) +const authorizeUser = isExternalAccessManagement + ? authorizeIntegration : authorize(); // Public routes: @@ -39,16 +37,16 @@ router // Protected routes: router .use(authService.authenticate('jwt')) - .get('/', authorizeAdminUser, processPagination(User), ctrl.list) - .post('/', authorizeAdminUser, ctrl.upsert) + .get('/', authorizeUser, processPagination(User), ctrl.list) + .post('/', authorizeUser, ctrl.upsert) .get('/logout', authService.logout()) .get('/me', ctrl.getProfile) .patch('/me', ctrl.updateProfile) .post('/me/change-password', ctrl.changePassword) - .delete('/:id', authorizeAdminUser, ctrl.remove) - .post('/:id/reinvite', authorizeAdminUser, ctrl.reinvite) - .post('/:id/tag', authorizeAdminUser, ctrl.addTag) - .delete('/:id/tag/:tagId', authorizeAdminUser, ctrl.removeTag); + .delete('/:id', authorizeUser, ctrl.remove) + .post('/:id/reinvite', authorizeUser, ctrl.reinvite) + .post('/:id/tag', authorizeUser, ctrl.addTag) + .delete('/:id/tag/:tagId', authorizeUser, ctrl.removeTag); export default { path: '/users', From 4653a7361e864539019d2552e5f173319c8f36e3 Mon Sep 17 00:00:00 2001 From: underscope Date: Mon, 11 Dec 2023 10:11:32 +0100 Subject: [PATCH 36/40] Resolve codacy issue --- server/user/userTag.model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/user/userTag.model.js b/server/user/userTag.model.js index 0bc730d3f..6418fbcc9 100644 --- a/server/user/userTag.model.js +++ b/server/user/userTag.model.js @@ -2,7 +2,7 @@ import { Model } from 'sequelize'; class UserTag extends Model { static fields({ INTEGER }) { - return { + return ({ userId: { type: INTEGER, field: 'user_id', @@ -15,7 +15,7 @@ class UserTag extends Model { primaryKey: true, unique: 'user_tag_pkey' } - }; + }); } static associate({ User, Tag }) { From e9f122c88e7c229b887df4fa0ed2d2d63f8492f6 Mon Sep 17 00:00:00 2001 From: underscope Date: Mon, 11 Dec 2023 10:15:01 +0100 Subject: [PATCH 37/40] Resolve codacy issues --- server/user/userTag.model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/user/userTag.model.js b/server/user/userTag.model.js index 6418fbcc9..5a46b4402 100644 --- a/server/user/userTag.model.js +++ b/server/user/userTag.model.js @@ -28,12 +28,12 @@ class UserTag extends Model { } static options() { - return { + return ({ modelName: 'UserTag', tableName: 'user_tag', underscored: true, timestamps: false - }; + }); } } From c35b614f88c90606271fad8209602904bfc4fc2b Mon Sep 17 00:00:00 2001 From: underscope Date: Tue, 12 Dec 2023 14:53:14 +0100 Subject: [PATCH 38/40] Fix issue due to lack of env parsing --- config/server/index.js | 8 ++++++-- server/repository/index.js | 5 +---- server/user/index.js | 5 +---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/config/server/index.js b/config/server/index.js index a14d9b197..0cfec2271 100644 --- a/config/server/index.js +++ b/config/server/index.js @@ -6,12 +6,14 @@ import * as store from './store.js'; import * as tce from './tce.js'; import isLocalhost from 'is-localhost'; import parse from 'url-parse'; +import yn from 'yn'; const hostname = resolveHostname(); const protocol = resolveProtocol(hostname); const port = resolvePort(); const origin = resolveOrigin(hostname, protocol, port); const previewUrl = process.env.PREVIEW_URL; +const isExternalAccessManagement = yn(process.env.EXTERNAL_ACCESS_MANAGEMENT); // Legacy config support function resolveHostname() { @@ -54,7 +56,8 @@ export { previewUrl, consumer, store, - tce + tce, + isExternalAccessManagement }; export default { @@ -68,5 +71,6 @@ export default { previewUrl, consumer, store, - tce + tce, + isExternalAccessManagement }; diff --git a/server/repository/index.js b/server/repository/index.js index 260f6fc38..0285edec5 100644 --- a/server/repository/index.js +++ b/server/repository/index.js @@ -5,6 +5,7 @@ import ctrl from './repository.controller.js'; import db from '../shared/database/index.js'; import express from 'express'; import feed from './feed/index.js'; +import { isExternalAccessManagement } from '../../config/server/index.js'; import multer from 'multer'; import path from 'node:path'; import processQuery from '../shared/util/processListQuery.js'; @@ -26,10 +27,6 @@ const { Repository, Tag } = db; const router = express.Router(); const { setSignedCookies } = proxyMw(storage, proxy); -const { - EXTERNAL_ACCESS_MANAGEMENT: isExternalAccessManagement -} = process.env; - const authorizeUser = isExternalAccessManagement ? authorizeIntegration : authorize(); diff --git a/server/user/index.js b/server/user/index.js index 7cbcabc1d..cc9535534 100644 --- a/server/user/index.js +++ b/server/user/index.js @@ -5,16 +5,13 @@ import authService from '../shared/auth/index.js'; import ctrl from './user.controller.js'; import db from '../shared/database/index.js'; import express from 'express'; +import { isExternalAccessManagement } from '../../config/server/index.js'; import { processPagination } from '../shared/database/pagination.js'; import { requestLimiter } from '../shared/request/mw.js'; const { User } = db; const router = express.Router(); -const { - EXTERNAL_ACCESS_MANAGEMENT: isExternalAccessManagement -} = process.env; - const authorizeUser = isExternalAccessManagement ? authorizeIntegration : authorize(); From 7adfcf0cd0254796a2f6270863f1d4fee1356f03 Mon Sep 17 00:00:00 2001 From: underscope Date: Wed, 13 Dec 2023 09:12:56 +0100 Subject: [PATCH 39/40] Restrict tag type update --- server/tag/tag.controller.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/tag/tag.controller.js b/server/tag/tag.controller.js index dac2d10a6..cabaaac32 100644 --- a/server/tag/tag.controller.js +++ b/server/tag/tag.controller.js @@ -1,5 +1,6 @@ +import { createError } from '../shared/error/helpers.js'; import db from '../shared/database/index.js'; -import { NO_CONTENT } from 'http-status-codes'; +import { BAD_REQUEST, NO_CONTENT } from 'http-status-codes'; import pick from 'lodash/pick.js'; import yn from 'yn'; @@ -24,9 +25,11 @@ function create({ body }, res) { } function patch({ tag, body }, res) { - const attrs = ['name', 'isAccessTag']; - const payload = pick(body, attrs); - return tag.update(payload) + if (Object.hasOwn(body, 'isAccessTag')) { + return createError(BAD_REQUEST, 'isAccessTag cannot be updated!'); + } + const { name } = body; + return tag.update({ name }) .then(tag => tag.reload()) .then(data => res.json({ data })); } From 438e2b1aec43661d990a3b4d8ef3c96b57d433a9 Mon Sep 17 00:00:00 2001 From: underscope Date: Wed, 13 Dec 2023 09:33:52 +0100 Subject: [PATCH 40/40] =?UTF-8?q?Lint=20=F0=9F=92=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/tag/tag.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/tag/tag.controller.js b/server/tag/tag.controller.js index cabaaac32..915943332 100644 --- a/server/tag/tag.controller.js +++ b/server/tag/tag.controller.js @@ -1,6 +1,6 @@ +import { BAD_REQUEST, NO_CONTENT } from 'http-status-codes'; import { createError } from '../shared/error/helpers.js'; import db from '../shared/database/index.js'; -import { BAD_REQUEST, NO_CONTENT } from 'http-status-codes'; import pick from 'lodash/pick.js'; import yn from 'yn';