snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
474
apps/meteor/ee/server/api/abac/index.ts
Normal file
474
apps/meteor/ee/server/api/abac/index.ts
Normal file
@@ -0,0 +1,474 @@
|
||||
import { AbacAttributeStoreExternalError, getPdpHealthErrorCode } from '@rocket.chat/abac';
|
||||
import { Abac } from '@rocket.chat/core-services';
|
||||
import type { AbacActor } from '@rocket.chat/core-services';
|
||||
import type { IServerEvents, IUser } from '@rocket.chat/core-typings';
|
||||
import { ServerEvents } from '@rocket.chat/models';
|
||||
import { validateUnauthorizedErrorResponse } from '@rocket.chat/rest-typings/src/v1/Ajv';
|
||||
import { convertSubObjectsIntoPaths } from '@rocket.chat/tools';
|
||||
|
||||
import {
|
||||
GenericSuccessSchema,
|
||||
PUTAbacAttributeUpdateBodySchema,
|
||||
GETAbacAttributesQuerySchema,
|
||||
GETAbacAttributesResponseSchema,
|
||||
GETAbacAttributeByIdResponseSchema,
|
||||
POSTAbacAttributeDefinitionSchema,
|
||||
GETAbacAttributeIsInUseResponseSchema,
|
||||
POSTRoomAbacAttributesBodySchema,
|
||||
POSTSingleRoomAbacAttributeBodySchema,
|
||||
PUTRoomAbacAttributeValuesBodySchema,
|
||||
POSTAbacUsersSyncBodySchema,
|
||||
GenericErrorSchema,
|
||||
GETAbacRoomsListQueryValidator,
|
||||
GETAbacRoomsResponseValidator,
|
||||
GETAbacAuditEventsQuerySchema,
|
||||
GETAbacAuditEventsResponseSchema,
|
||||
GETAbacPdpHealthResponseSchema,
|
||||
GETAbacPdpHealthErrorResponseSchema,
|
||||
} from './schemas';
|
||||
import { API } from '../../../../server/api';
|
||||
import type { ExtractRoutesFromAPI } from '../../../../server/api/ApiClass';
|
||||
import { getPaginationItems } from '../../../../server/api/lib/getPaginationItems';
|
||||
import { settings } from '../../../../server/settings';
|
||||
|
||||
const getActorFromUser = (user?: IUser | null): AbacActor | undefined =>
|
||||
user?._id
|
||||
? {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const assertLocalAttributeStore = async (): Promise<void> => {
|
||||
if (await Abac.isExternalAttributeStore()) {
|
||||
throw new AbacAttributeStoreExternalError();
|
||||
}
|
||||
};
|
||||
|
||||
const abacEndpoints = API.v1
|
||||
.post(
|
||||
'abac/rooms/:rid/attributes',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-rooms'],
|
||||
body: POSTRoomAbacAttributesBodySchema,
|
||||
response: {
|
||||
200: GenericSuccessSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
license: ['abac'],
|
||||
},
|
||||
async function action() {
|
||||
const { rid } = this.urlParams;
|
||||
const { attributes } = this.bodyParams;
|
||||
|
||||
if (!settings.get('ABAC_Enabled')) {
|
||||
throw new Error('error-abac-not-enabled');
|
||||
}
|
||||
|
||||
// This is a replace-all operation
|
||||
// IF you need fine grained, use the other endpoints for removing, editing & adding single attributes
|
||||
await Abac.setRoomAbacAttributes(rid, attributes, getActorFromUser(this.user));
|
||||
return API.v1.success();
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
'abac/rooms/:rid/attributes',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-rooms'],
|
||||
response: {
|
||||
200: GenericSuccessSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { rid } = this.urlParams;
|
||||
|
||||
// We don't need to check if ABAC is enabled to clear attributes
|
||||
// Since we're always allowing this operation
|
||||
// license check is also not required
|
||||
await Abac.setRoomAbacAttributes(rid, {}, getActorFromUser(this.user));
|
||||
return API.v1.success();
|
||||
},
|
||||
)
|
||||
// add an abac attribute by key
|
||||
.post(
|
||||
'abac/rooms/:rid/attributes/:key',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-rooms'],
|
||||
license: ['abac'],
|
||||
body: POSTSingleRoomAbacAttributeBodySchema,
|
||||
response: {
|
||||
200: GenericSuccessSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { rid, key } = this.urlParams;
|
||||
const { values } = this.bodyParams;
|
||||
|
||||
if (!settings.get('ABAC_Enabled')) {
|
||||
throw new Error('error-abac-not-enabled');
|
||||
}
|
||||
|
||||
await Abac.addRoomAbacAttributeByKey(rid, key, values, getActorFromUser(this.user));
|
||||
return API.v1.success();
|
||||
},
|
||||
)
|
||||
// edit a room attribute
|
||||
.put(
|
||||
'abac/rooms/:rid/attributes/:key',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-rooms'],
|
||||
body: PUTRoomAbacAttributeValuesBodySchema,
|
||||
response: {
|
||||
200: GenericSuccessSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
license: ['abac'],
|
||||
},
|
||||
async function action() {
|
||||
const { rid, key } = this.urlParams;
|
||||
const { values } = this.bodyParams;
|
||||
|
||||
if (!settings.get('ABAC_Enabled')) {
|
||||
throw new Error('error-abac-not-enabled');
|
||||
}
|
||||
|
||||
await Abac.replaceRoomAbacAttributeByKey(rid, key, values, getActorFromUser(this.user));
|
||||
return API.v1.success();
|
||||
},
|
||||
)
|
||||
// delete a room attribute
|
||||
.delete(
|
||||
'abac/rooms/:rid/attributes/:key',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-rooms'],
|
||||
response: {
|
||||
200: GenericSuccessSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { rid, key } = this.urlParams;
|
||||
|
||||
await Abac.removeRoomAbacAttribute(rid, key, getActorFromUser(this.user));
|
||||
return API.v1.success();
|
||||
},
|
||||
)
|
||||
// attribute endpoints
|
||||
// list attributes
|
||||
.get(
|
||||
'abac/attributes',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'],
|
||||
query: GETAbacAttributesQuerySchema,
|
||||
response: {
|
||||
200: GETAbacAttributesResponseSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams as Record<string, string | string[] | number | null | undefined>);
|
||||
const { key, values } = this.queryParams;
|
||||
|
||||
return API.v1.success(
|
||||
await Abac.listAbacAttributes(
|
||||
{
|
||||
key,
|
||||
values,
|
||||
offset,
|
||||
count,
|
||||
},
|
||||
getActorFromUser(this.user),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
.post(
|
||||
'abac/users/sync',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'],
|
||||
license: ['abac'],
|
||||
body: POSTAbacUsersSyncBodySchema,
|
||||
response: {
|
||||
200: GenericSuccessSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
if (!settings.get('ABAC_Enabled')) {
|
||||
throw new Error('error-abac-not-enabled');
|
||||
}
|
||||
|
||||
const { usernames, ids, emails, ldapIds } = this.bodyParams;
|
||||
|
||||
await Abac.reevaluateUsers({ usernames, ids, emails, ldapIds });
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
)
|
||||
.post(
|
||||
'abac/attributes',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'],
|
||||
license: ['abac'],
|
||||
body: POSTAbacAttributeDefinitionSchema,
|
||||
response: {
|
||||
200: GenericSuccessSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
if (!settings.get('ABAC_Enabled')) {
|
||||
throw new Error('error-abac-not-enabled');
|
||||
}
|
||||
|
||||
await assertLocalAttributeStore();
|
||||
|
||||
await Abac.addAbacAttribute(this.bodyParams, getActorFromUser(this.user));
|
||||
return API.v1.success();
|
||||
},
|
||||
)
|
||||
// update attribute definition (key and/or values)
|
||||
.put(
|
||||
'abac/attributes/:_id',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'],
|
||||
license: ['abac'],
|
||||
body: PUTAbacAttributeUpdateBodySchema,
|
||||
response: {
|
||||
200: GenericSuccessSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { _id } = this.urlParams;
|
||||
if (!settings.get('ABAC_Enabled')) {
|
||||
throw new Error('error-abac-not-enabled');
|
||||
}
|
||||
|
||||
await assertLocalAttributeStore();
|
||||
|
||||
await Abac.updateAbacAttributeById(_id, this.bodyParams, getActorFromUser(this.user));
|
||||
return API.v1.success();
|
||||
},
|
||||
)
|
||||
// get single attribute with usage
|
||||
.get(
|
||||
'abac/attributes/:_id',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'],
|
||||
response: {
|
||||
200: GETAbacAttributeByIdResponseSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { _id } = this.urlParams;
|
||||
|
||||
await assertLocalAttributeStore();
|
||||
|
||||
const result = await Abac.getAbacAttributeById(_id, getActorFromUser(this.user));
|
||||
return API.v1.success(result);
|
||||
},
|
||||
)
|
||||
// delete attribute (only if not in use)
|
||||
.delete(
|
||||
'abac/attributes/:_id',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'],
|
||||
response: {
|
||||
200: GenericSuccessSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { _id } = this.urlParams;
|
||||
|
||||
await assertLocalAttributeStore();
|
||||
|
||||
await Abac.deleteAbacAttributeById(_id, getActorFromUser(this.user));
|
||||
return API.v1.success();
|
||||
},
|
||||
)
|
||||
// check if attribute is in use
|
||||
.get(
|
||||
'abac/attributes/:key/is-in-use',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'],
|
||||
response: {
|
||||
200: GETAbacAttributeIsInUseResponseSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { key } = this.urlParams;
|
||||
|
||||
await assertLocalAttributeStore();
|
||||
|
||||
const inUse = await Abac.isAbacAttributeInUseByKey(key);
|
||||
return API.v1.success({ inUse });
|
||||
},
|
||||
)
|
||||
.get(
|
||||
'abac/rooms',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-rooms'],
|
||||
response: {
|
||||
200: GETAbacRoomsResponseValidator,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
400: GenericErrorSchema,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
query: GETAbacRoomsListQueryValidator,
|
||||
},
|
||||
async function action() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams as Record<string, string | string[] | number | null | undefined>);
|
||||
const { filter, filterType } = this.queryParams;
|
||||
|
||||
const result = await Abac.listAbacRooms(
|
||||
{
|
||||
offset,
|
||||
count,
|
||||
filter,
|
||||
filterType,
|
||||
},
|
||||
getActorFromUser(this.user),
|
||||
);
|
||||
|
||||
return API.v1.success(result);
|
||||
},
|
||||
)
|
||||
.get(
|
||||
'abac/pdp/health',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'manage-abac-admin-settings'],
|
||||
rateLimiterOptions: {
|
||||
numRequestsAllowed: 5,
|
||||
intervalTimeInMS: 60000,
|
||||
},
|
||||
response: {
|
||||
200: GETAbacPdpHealthResponseSchema,
|
||||
400: GETAbacPdpHealthErrorResponseSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
try {
|
||||
await Abac.getPDPHealth();
|
||||
return API.v1.success({ available: true, message: 'ABAC_PDP_Health_OK' });
|
||||
} catch (err) {
|
||||
return API.v1.failure({ available: false, message: getPdpHealthErrorCode(err) });
|
||||
}
|
||||
},
|
||||
)
|
||||
.get(
|
||||
'abac/audit',
|
||||
{
|
||||
response: {
|
||||
200: GETAbacAuditEventsResponseSchema,
|
||||
400: GenericErrorSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
query: GETAbacAuditEventsQuerySchema,
|
||||
authRequired: true,
|
||||
permissionsRequired: ['abac-management', 'view-abac-admin-audit'],
|
||||
license: ['abac', 'auditing'],
|
||||
},
|
||||
async function action() {
|
||||
const { start, end, actor } = this.queryParams;
|
||||
|
||||
const { offset, count } = await getPaginationItems(this.queryParams as Record<string, string | number | null | undefined>);
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
const _sort = { ts: sort?.ts ? sort?.ts : -1 };
|
||||
|
||||
const { cursor, totalCount } = ServerEvents.findPaginated(
|
||||
{
|
||||
...(actor && convertSubObjectsIntoPaths({ actor })),
|
||||
ts: {
|
||||
$gte: start ? new Date(start) : new Date(0),
|
||||
$lte: end ? new Date(end) : new Date(),
|
||||
},
|
||||
t: {
|
||||
$in: [
|
||||
'abac.attribute.changed',
|
||||
'abac.object.attribute.changed',
|
||||
'abac.object.attributes.removed',
|
||||
'abac.action.performed',
|
||||
'abac.attribute.store.switched',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
sort: _sort,
|
||||
skip: offset,
|
||||
limit: count,
|
||||
allowDiskUse: true,
|
||||
},
|
||||
);
|
||||
|
||||
const [events, total] = await Promise.all([cursor.toArray(), totalCount]);
|
||||
|
||||
return API.v1.success({
|
||||
events: events as (
|
||||
| IServerEvents['abac.action.performed']
|
||||
| IServerEvents['abac.attribute.changed']
|
||||
| IServerEvents['abac.object.attribute.changed']
|
||||
| IServerEvents['abac.object.attributes.removed']
|
||||
| IServerEvents['abac.attribute.store.switched']
|
||||
)[],
|
||||
count: events.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export type AbacEndpoints = ExtractRoutesFromAPI<typeof abacEndpoints>;
|
||||
|
||||
declare module '@rocket.chat/rest-typings' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
|
||||
interface Endpoints extends AbacEndpoints {}
|
||||
}
|
||||
422
apps/meteor/ee/server/api/abac/schemas.ts
Normal file
422
apps/meteor/ee/server/api/abac/schemas.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
import type {
|
||||
IAbacAttribute,
|
||||
IAbacAttributeDefinition,
|
||||
IAuditServerActor,
|
||||
IRoom,
|
||||
IRoomAbacRedaction,
|
||||
IServerEvents,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import type { PaginatedResult, PaginatedRequest } from '@rocket.chat/rest-typings';
|
||||
import { ajv, ajvQuery } from '@rocket.chat/rest-typings';
|
||||
|
||||
const ATTRIBUTE_KEY_PATTERN = '^[A-Za-z0-9_-]+$';
|
||||
const MAX_ROOM_ATTRIBUTE_VALUES = 10;
|
||||
const MAX_USERS_SYNC_ITEMS = 100;
|
||||
const MAX_ROOM_ATTRIBUTE_KEYS = 10;
|
||||
|
||||
const GenericSuccess = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', enum: [true] },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const GenericSuccessSchema = ajv.compile<void>(GenericSuccess);
|
||||
|
||||
// Update ABAC attribute (request body)
|
||||
const UpdateAbacAttributeBody = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
values: {
|
||||
type: 'array',
|
||||
items: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
minItems: 1,
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
anyOf: [{ required: ['key'] }, { required: ['values'] }],
|
||||
};
|
||||
|
||||
export const PUTAbacAttributeUpdateBodySchema = ajv.compile<IAbacAttributeDefinition>(UpdateAbacAttributeBody);
|
||||
|
||||
const AbacAttributeDefinition = {
|
||||
type: 'object',
|
||||
|
||||
properties: {
|
||||
key: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
values: {
|
||||
type: 'array',
|
||||
items: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
minItems: 1,
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
required: ['key', 'values'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const POSTAbacAttributeDefinitionSchema = ajv.compile<IAbacAttributeDefinition>(AbacAttributeDefinition);
|
||||
|
||||
const GetAbacAttributesQuery = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
values: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
offset: { type: 'number' },
|
||||
count: { type: 'number' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const GETAbacAttributesQuerySchema = ajvQuery.compile<{ key?: string; values?: string; offset: number; count?: number }>(
|
||||
GetAbacAttributesQuery,
|
||||
);
|
||||
|
||||
const AbacAttributeRecord = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
_id: { type: 'string', minLength: 1 },
|
||||
key: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
values: {
|
||||
type: 'array',
|
||||
items: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
minItems: 1,
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
required: ['_id', 'key', 'values'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const GetAbacAttributesResponse = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', enum: [true] },
|
||||
attributes: {
|
||||
type: 'array',
|
||||
items: AbacAttributeRecord,
|
||||
},
|
||||
offset: { type: 'number' },
|
||||
count: { type: 'number' },
|
||||
total: { type: 'number' },
|
||||
},
|
||||
required: ['attributes', 'offset', 'count', 'total'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const GETAbacAttributesResponseSchema = ajv.compile<{
|
||||
attributes: Pick<IAbacAttribute, '_id' | 'key' | 'values'>[];
|
||||
offset: number;
|
||||
count: number;
|
||||
total: number;
|
||||
}>(GetAbacAttributesResponse);
|
||||
|
||||
const GetAbacAttributeByIdResponse = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', enum: [true] },
|
||||
_id: { type: 'string', minLength: 1 },
|
||||
key: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
values: {
|
||||
type: 'array',
|
||||
items: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
minItems: 1,
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
required: ['key', 'values'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const GETAbacAttributeByIdResponseSchema = ajv.compile<{
|
||||
key: string;
|
||||
values: string[];
|
||||
}>(GetAbacAttributeByIdResponse);
|
||||
|
||||
const GetAbacAttributeIsInUseResponse = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', enum: [true] },
|
||||
inUse: { type: 'boolean' },
|
||||
},
|
||||
required: ['inUse'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const GETAbacAttributeIsInUseResponseSchema = ajv.compile<{ inUse: boolean }>(GetAbacAttributeIsInUseResponse);
|
||||
|
||||
const GetAbacAuditEventsQuerySchemaObject = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
start: { type: 'string', format: 'date-time', nullable: true },
|
||||
end: { type: 'string', format: 'date-time', nullable: true },
|
||||
offset: { type: 'number', nullable: true },
|
||||
count: { type: 'number', nullable: true },
|
||||
actor: {
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
_id: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
ip: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
useragent: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const GETAbacAuditEventsQuerySchema = ajvQuery.compile<
|
||||
PaginatedRequest<{
|
||||
start?: string;
|
||||
end?: string;
|
||||
actor?: IAuditServerActor;
|
||||
}>
|
||||
>(GetAbacAuditEventsQuerySchemaObject);
|
||||
|
||||
const GetAbacAuditEventsResponseSchemaObject = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', enum: [true] },
|
||||
events: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'The number of events returned in this response.',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
description: 'The number of events that were skipped in this response.',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'The total number of events that match the query.',
|
||||
},
|
||||
},
|
||||
required: ['events', 'count', 'offset', 'total'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const GETAbacAuditEventsResponseSchema = ajv.compile<{
|
||||
events: (
|
||||
| IServerEvents['abac.action.performed']
|
||||
| IServerEvents['abac.attribute.changed']
|
||||
| IServerEvents['abac.object.attribute.changed']
|
||||
| IServerEvents['abac.object.attributes.removed']
|
||||
| IServerEvents['abac.attribute.store.switched']
|
||||
)[];
|
||||
count: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
}>(GetAbacAuditEventsResponseSchemaObject);
|
||||
|
||||
const PostRoomAbacAttributesBody = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
attributes: {
|
||||
type: 'object',
|
||||
propertyNames: { type: 'string', pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
minProperties: 1,
|
||||
maxProperties: MAX_ROOM_ATTRIBUTE_KEYS,
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
items: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
maxItems: MAX_ROOM_ATTRIBUTE_VALUES,
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['attributes'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const POSTRoomAbacAttributesBodySchema = ajv.compile<{ attributes: Record<string, string[]> }>(PostRoomAbacAttributesBody);
|
||||
|
||||
const PostSingleRoomAbacAttributeBody = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
values: {
|
||||
type: 'array',
|
||||
items: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
minItems: 1,
|
||||
maxItems: MAX_ROOM_ATTRIBUTE_VALUES,
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
required: ['values'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const POSTSingleRoomAbacAttributeBodySchema = ajv.compile<{ values: string[] }>(PostSingleRoomAbacAttributeBody);
|
||||
|
||||
const PutRoomAbacAttributeValuesBody = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
values: {
|
||||
type: 'array',
|
||||
items: { type: 'string', minLength: 1, pattern: ATTRIBUTE_KEY_PATTERN },
|
||||
minItems: 1,
|
||||
maxItems: MAX_ROOM_ATTRIBUTE_VALUES,
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
required: ['values'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const PUTRoomAbacAttributeValuesBodySchema = ajv.compile<{ values: string[] }>(PutRoomAbacAttributeValuesBody);
|
||||
|
||||
const GenericError = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const PostAbacUsersSyncBody = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
usernames: {
|
||||
type: 'array',
|
||||
items: { type: 'string', minLength: 1 },
|
||||
minItems: 1,
|
||||
maxItems: MAX_USERS_SYNC_ITEMS,
|
||||
uniqueItems: true,
|
||||
},
|
||||
ids: {
|
||||
type: 'array',
|
||||
items: { type: 'string', minLength: 1 },
|
||||
minItems: 1,
|
||||
maxItems: MAX_USERS_SYNC_ITEMS,
|
||||
uniqueItems: true,
|
||||
},
|
||||
emails: {
|
||||
type: 'array',
|
||||
items: { type: 'string', minLength: 1 },
|
||||
minItems: 1,
|
||||
maxItems: MAX_USERS_SYNC_ITEMS,
|
||||
uniqueItems: true,
|
||||
},
|
||||
ldapIds: {
|
||||
type: 'array',
|
||||
items: { type: 'string', minLength: 1 },
|
||||
minItems: 1,
|
||||
maxItems: MAX_USERS_SYNC_ITEMS,
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
anyOf: [{ required: ['usernames'] }, { required: ['ids'] }, { required: ['emails'] }, { required: ['ldapIds'] }],
|
||||
};
|
||||
|
||||
export const POSTAbacUsersSyncBodySchema = ajv.compile<{
|
||||
usernames?: string[];
|
||||
ids?: string[];
|
||||
emails?: string[];
|
||||
ldapIds?: string[];
|
||||
}>(PostAbacUsersSyncBody);
|
||||
|
||||
export const GenericErrorSchema = ajv.compile<{ success: boolean; message: string }>(GenericError);
|
||||
|
||||
export const GETAbacPdpHealthResponseSchema = ajv.compile<{
|
||||
available: boolean;
|
||||
message: string;
|
||||
}>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', enum: [true] },
|
||||
available: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
},
|
||||
required: ['success', 'available', 'message'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
export const GETAbacPdpHealthErrorResponseSchema = ajv.compile<{ available: boolean; message: string }>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', enum: [false] },
|
||||
available: { type: 'boolean', enum: [false] },
|
||||
message: { type: 'string' },
|
||||
},
|
||||
required: ['success', 'available', 'message'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
const GETAbacRoomsListQuerySchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
filter: { type: 'string', minLength: 1 },
|
||||
filterType: { type: 'string', enum: ['all', 'roomName', 'attribute', 'value'] },
|
||||
offset: { type: 'number' },
|
||||
count: { type: 'number' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
type GETAbacRoomsListQuery = PaginatedRequest<{ filter?: string; filterType?: 'all' | 'roomName' | 'attribute' | 'value' }>;
|
||||
|
||||
export const GETAbacRoomsListQueryValidator = ajvQuery.compile<GETAbacRoomsListQuery>(GETAbacRoomsListQuerySchema);
|
||||
|
||||
export const GETAbacRoomsResponseSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
enum: [true],
|
||||
},
|
||||
rooms: {
|
||||
type: 'array',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
required: ['rooms', 'offset', 'count', 'total'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
type GETAbacRoomsResponse = PaginatedResult<{
|
||||
rooms: Array<IRoom & IRoomAbacRedaction>;
|
||||
}>;
|
||||
|
||||
export const GETAbacRoomsResponseValidator = ajv.compile<GETAbacRoomsResponse>(GETAbacRoomsResponseSchema);
|
||||
Reference in New Issue
Block a user