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/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 @@
- {{ truncatedName }}
+
+ mdi-account-supervisor
+ {{ truncatedName }}
+
{{ name }}
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..00b6cdea9 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.isExternalAccessManagement
+ ? []
+ : [{ label: 'Clone', icon: 'content-copy', name: 'clone' }];
+ return [
+ ...defaultEntries,
+ ...conditionalEntries
+ ];
}
}
};
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 }
};
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/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' }
};
diff --git a/server/repository/index.js b/server/repository/index.js
index e7ef16ca0..0285edec5 100644
--- a/server/repository/index.js
+++ b/server/repository/index.js
@@ -1,15 +1,17 @@
+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';
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';
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,19 +19,24 @@ 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 */
-const { Repository } = db;
+const { Repository, Tag } = db;
const router = express.Router();
const { setSignedCookies } = proxyMw(storage, proxy);
+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', authorize(), upload.single('archive'), ctrl.import);
+ .post('/import', authorizeUser, upload.single('archive'), ctrl.import);
router
.param('repositoryId', getRepository)
@@ -37,7 +44,7 @@ router
router.route('/')
.get(processQuery({ limit: 100 }), ctrl.index)
- .post(authorize(), ctrl.create);
+ .post(authorizeUser, ctrl.create);
router.route('/:repositoryId')
.get(ctrl.get)
@@ -46,15 +53,15 @@ router.route('/:repositoryId')
router
.post('/:repositoryId/pin', ctrl.pin)
- .post('/:repositoryId/clone', authorize(), 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', ctrl.upsertUser)
- .delete('/:repositoryId/users/:userId', ctrl.removeUser)
- .post('/:repositoryId/tags', ctrl.addTag)
- .delete('/:repositoryId/tags/:tagId', 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);
@@ -68,7 +75,8 @@ function mount(router, mountPath, subrouter) {
}
function getRepository(req, _res, next, repositoryId) {
- return Repository.findByPk(repositoryId, { paranoid: false })
+ return Repository
+ .findByPk(repositoryId, { include: [{ model: Tag }], paranoid: false })
.then(repository => repository || createError(NOT_FOUND, 'Repository not found'))
.then(repository => {
req.repository = repository;
@@ -79,6 +87,13 @@ function getRepository(req, _res, next, repositoryId) {
function hasAccess(req, _res, next) {
const { user, repository } = req;
if (user.isAdmin()) 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 => {
diff --git a/server/repository/repository.controller.js b/server/repository/repository.controller.js
index 8a40899bf..8fa40d794 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,27 @@ 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) return createError(NOT_FOUND, 'Tag not found');
+ if (tag.isAccessTag && user.role !== userRole.INTEGRATION) {
+ return createError(FORBIDDEN, 'Can be removed only by integration users');
+ }
const where = { tagId, repositoryId };
await RepositoryTag.destroy({ where });
return res.status(NO_CONTENT).send();
@@ -222,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();
});
}
diff --git a/server/shared/auth/index.js b/server/shared/auth/index.js
index a1a8e6931..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));
@@ -27,7 +29,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));
@@ -50,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));
}
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/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),
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/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/index.js b/server/tag/index.js
index 90257d38c..2133be6a0 100644
--- a/server/tag/index.js
+++ b/server/tag/index.js
@@ -1,10 +1,35 @@
+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';
const router = express.Router();
+const { Tag } = db;
router
- .get('/', ctrl.list);
+ .get('/', ctrl.list)
+ .post('/', authorizeIntegration, ctrl.create);
+
+router
+ .param('tagId', getTag)
+ .use('/:tagId', authorizeIntegration);
+
+router.route('/:tagId')
+ .get(ctrl.get)
+ .patch(ctrl.patch)
+ .delete(ctrl.remove);
+
+function getTag(req, _res, next, tagId) {
+ return Tag
+ .findByPk(tagId)
+ .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..915943332 100644
--- a/server/tag/tag.controller.js
+++ b/server/tag/tag.controller.js
@@ -1,4 +1,7 @@
+import { BAD_REQUEST, NO_CONTENT } from 'http-status-codes';
+import { createError } from '../shared/error/helpers.js';
import db from '../shared/database/index.js';
+import pick from 'lodash/pick.js';
import yn from 'yn';
const { Tag } = db;
@@ -10,6 +13,36 @@ 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) {
+ 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 }));
+}
+
+function remove({ tag }, res) {
+ return tag.destroy()
+ .then(() => res.status(NO_CONTENT).end());
+}
+
export default {
- list
+ list,
+ get,
+ create,
+ patch,
+ remove
};
diff --git a/server/tag/tag.model.js b/server/tag/tag.model.js
index b2997ff28..067458eb6 100644
--- a/server/tag/tag.model.js
+++ b/server/tag/tag.model.js
@@ -1,7 +1,8 @@
import { Model } from 'sequelize';
+import { user as userRole } from '../../config/shared/role.js';
class Tag extends Model {
- static fields({ STRING, UUID, UUIDV4 }) {
+ static fields({ BOOLEAN, STRING, UUID, UUIDV4 }) {
return {
uid: {
type: UUID,
@@ -14,15 +15,34 @@ class Tag extends Model {
allowNull: false,
unique: true,
validate: { notEmpty: true, len: [2, 20] }
+ },
+ isAccessTag: {
+ type: BOOLEAN,
+ field: 'is_access_tag',
+ defaultValue: false
}
};
}
- 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 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) {
@@ -36,9 +56,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/index.js b/server/user/index.js
index 195d481d0..cc9535534 100644
--- a/server/user/index.js
+++ b/server/user/index.js
@@ -1,16 +1,21 @@
+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 { 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 authorizeUser = isExternalAccessManagement
+ ? authorizeIntegration
+ : authorize();
+
// Public routes:
router
.post(
@@ -29,14 +34,16 @@ router
// Protected routes:
router
.use(authService.authenticate('jwt'))
- .get('/', authorize(), processPagination(User), ctrl.list)
- .post('/', authorize(), 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', authorize(), ctrl.remove)
- .post('/:id/reinvite', authorize(), ctrl.reinvite);
+ .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',
diff --git a/server/user/user.controller.js b/server/user/user.controller.js
index 1af261c1b..7a8b738db 100644
--- a/server/user/user.controller.js
+++ b/server/user/user.controller.js
@@ -1,10 +1,11 @@
-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 { User } = db;
+const { sequelize, Tag, User, UserTag } = db;
const createFilter = q => map(['email', 'firstName', 'lastName'],
it => ({ [it]: { [Op.iLike]: `%${q}%` } }));
@@ -69,6 +70,36 @@ function reinvite({ params }, res) {
.then(() => res.status(ACCEPTED).end());
}
+function addTag(
+ {
+ user,
+ body: { name, isAccessTag },
+ params: { id: userId }
+ },
+ res
+) {
+ return sequelize.transaction(async 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, 'Only integration users can remove access tags');
+ }
+ const where = { tagId, userId };
+ await UserTag.destroy({ where });
+ return res.status(NO_CONTENT).send();
+}
+
export default {
list,
upsert,
@@ -78,5 +109,7 @@ export default {
getProfile,
updateProfile,
changePassword,
- reinvite
+ reinvite,
+ addTag,
+ removeTag
};
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;
diff --git a/server/user/userTag.model.js b/server/user/userTag.model.js
new file mode 100644
index 000000000..5a46b4402
--- /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;
diff --git a/tools/integration-user/README.md b/tools/integration-user/README.md
new file mode 100644
index 000000000..73949b822
--- /dev/null
+++ b/tools/integration-user/README.md
@@ -0,0 +1,25 @@
+# Integration API
+
+## Add integration user
+
+To add integration user run:
+
+```shell
+npm run integration:add
+```
+
+to generate a integration token run:
+
+```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.
+
+## External access management
+
+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 user management UI.
diff --git a/tools/integration-user/integration_user_API.postman_collection.json b/tools/integration-user/integration_user_API.postman_collection.json
new file mode 100644
index 000000000..0db703761
--- /dev/null
+++ b/tools/integration-user/integration_user_API.postman_collection.json
@@ -0,0 +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\",\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
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),