Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 4 additions & 28 deletions apps/meteor/app/api/server/ApiClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import type { RateLimiterOptionsToCheck } from 'meteor/rate-limit';
import { RateLimiter } from 'meteor/rate-limit';
import _ from 'underscore';

import type { PermissionsPayload } from './api.helpers';
import { checkPermissionsForInvocation, checkPermissions, parseDeprecation } from './api.helpers';
import { checkPermissions, parseDeprecation } from './api.helpers';
import type {
FailureResult,
ForbiddenResult,
Expand All @@ -42,6 +41,7 @@ import type {
import { getUserInfo } from './helpers/getUserInfo';
import { parseJsonQuery } from './helpers/parseJsonQuery';
import { authenticationMiddlewareForHono } from './middlewares/authenticationHono';
import { permissionsMiddleware } from './middlewares/permissions';
import type { APIActionContext } from './router';
import { RocketChatAPIRouter } from './router';
import { license } from '../../../ee/app/api-enterprise/server/middlewares/license';
Expand Down Expand Up @@ -781,7 +781,7 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<

const operations = endpoints;

const shouldVerifyPermissions = checkPermissions(options);
checkPermissions(options);

// Allow for more than one route using the same option and endpoints
if (!Array.isArray(subpaths)) {
Expand Down Expand Up @@ -856,31 +856,6 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
throw new Meteor.Error('invalid-params', validatorFunc.errors?.map((error: any) => error.message).join('\n '));
}
}
if (shouldVerifyPermissions) {
if (!this.userId) {
if (applyBreakingChanges) {
throw new Meteor.Error('error-unauthorized', 'You must be logged in to do this');
}
throw new Meteor.Error('error-unauthorized', 'User does not have the permissions required for this action');
}
if (
!(await checkPermissionsForInvocation(
this.userId,
_options.permissionsRequired as PermissionsPayload,
this.request.method as Method,
))
) {
if (applyBreakingChanges) {
throw new Meteor.Error('error-forbidden', 'User does not have the permissions required for this action', {
permissions: _options.permissionsRequired,
});
}
throw new Meteor.Error('error-unauthorized', 'User does not have the permissions required for this action', {
permissions: _options.permissionsRequired,
});
}
}

if (
this.userId &&
(await api.processTwoFactor({
Expand Down Expand Up @@ -941,6 +916,7 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
userWithoutUsername: options.userWithoutUsername,
logger,
}),
permissionsMiddleware(_options as TypedOptions),
license(_options as TypedOptions, License),
(operations[method as keyof Operations<TPathPattern, TOptions>] as Record<string, any>).action,
);
Expand Down
56 changes: 56 additions & 0 deletions apps/meteor/app/api/server/middlewares/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Logger } from '@rocket.chat/logger';
import type { Method } from '@rocket.chat/rest-typings';
import type { MiddlewareHandler } from 'hono';

import { applyBreakingChanges } from '../ApiClass';
import { API } from '../api';
import { type PermissionsPayload, checkPermissionsForInvocation } from '../api.helpers';
import type { TypedOptions } from '../definition';
import type { HonoContext } from '../router';

const logger = new Logger('PermissionsMiddleware');

export const permissionsMiddleware =
(options: TypedOptions): MiddlewareHandler =>
async (c: HonoContext, next) => {
if (!options.permissionsRequired) {
return next();
}

const user = c.get('user');

if (!user) {
if (applyBreakingChanges) {
const unauthorized = API.v1.unauthorized('You must be logged in to do this');
return c.json(unauthorized.body, unauthorized.statusCode);
}

const failure = API.v1.forbidden('User does not have the permissions required for this action [error-unauthorized]');
return c.json(failure.body, failure.statusCode);
}

let hasPermission: boolean;
try {
hasPermission = await checkPermissionsForInvocation(
user._id,
options.permissionsRequired as PermissionsPayload,
c.req.method as Method,
);
} catch (e) {
logger.error({ msg: 'Error checking permissions for invocation', err: e });
const error = API.v1.internalError();
return c.json(error.body, error.statusCode);
}

if (!hasPermission) {
if (applyBreakingChanges) {
const forbidden = API.v1.forbidden('User does not have the permissions required for this action [error-unauthorized]');
return c.json(forbidden.body, forbidden.statusCode);
}

const failure = API.v1.forbidden('User does not have the permissions required for this action [error-unauthorized]');
return c.json(failure.body, failure.statusCode);
}

return next();
};
24 changes: 19 additions & 5 deletions apps/meteor/app/api/server/v1/push.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Push } from '@rocket.chat/core-services';
import type { IPushToken } from '@rocket.chat/core-typings';
import type { IPushToken, IPushTokenTypes } from '@rocket.chat/core-typings';
import { Messages, PushToken, Users, Rooms, Settings } from '@rocket.chat/models';
import {
ajv,
Expand All @@ -23,9 +23,10 @@ import type { SuccessResult } from '../definition';

type PushTokenPOST = {
id?: string;
type: 'apn' | 'gcm';
type: IPushTokenTypes;
value: string;
appName: string;
voipToken?: string;
};

const PushTokenPOSTSchema: JSONSchemaType<PushTokenPOST> = {
Expand All @@ -47,6 +48,10 @@ const PushTokenPOSTSchema: JSONSchemaType<PushTokenPOST> = {
type: 'string',
minLength: 1,
},
voipToken: {
type: 'string',
nullable: true,
},
},
required: ['type', 'value', 'appName'],
additionalProperties: false,
Expand All @@ -72,13 +77,13 @@ const PushTokenDELETESchema: JSONSchemaType<PushTokenDELETE> = {

export const isPushTokenDELETEProps = ajv.compile<PushTokenDELETE>(PushTokenDELETESchema);

type PushTokenResult = Pick<IPushToken, '_id' | 'token' | 'appName' | 'userId' | 'enabled' | 'createdAt' | '_updatedAt'>;
type PushTokenResult = Pick<IPushToken, '_id' | 'token' | 'appName' | 'userId' | 'enabled' | 'createdAt' | '_updatedAt' | 'voipToken'>;

/**
* Pick only the attributes we actually want to return on the endpoint, ensuring nothing from older schemas get mixed in
*/
function cleanTokenResult(result: Omit<IPushToken, 'authToken'>): PushTokenResult {
const { _id, token, appName, userId, enabled, createdAt, _updatedAt } = result;
const { _id, token, appName, userId, enabled, createdAt, _updatedAt, voipToken } = result;

return {
_id,
Expand All @@ -88,6 +93,7 @@ function cleanTokenResult(result: Omit<IPushToken, 'authToken'>): PushTokenResul
enabled,
createdAt,
_updatedAt,
voipToken,
};
}

Expand Down Expand Up @@ -140,6 +146,9 @@ const pushTokenEndpoints = API.v1
_updatedAt: {
type: 'string',
},
voipToken: {
type: 'string',
},
},
additionalProperties: false,
},
Expand All @@ -154,7 +163,11 @@ const pushTokenEndpoints = API.v1
authRequired: true,
},
async function action() {
const { id, type, value, appName } = this.bodyParams;
const { id, type, value, appName, voipToken } = this.bodyParams;

if (voipToken && !id) {
return API.v1.failure('voip-tokens-must-specify-device-id');
}

const rawToken = this.request.headers.get('x-auth-token');
if (!rawToken) {
Expand All @@ -168,6 +181,7 @@ const pushTokenEndpoints = API.v1
authToken,
appName,
userId: this.userId,
...(voipToken && { voipToken }),
});

return API.v1.success({ result: cleanTokenResult(result) });
Expand Down
26 changes: 13 additions & 13 deletions apps/meteor/app/file-upload/server/config/AmazonS3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const configure = _.debounce(() => {
const AWSSecretAccessKey = settings.get<string>('FileUpload_S3_AWSSecretAccessKey');
const URLExpiryTimeSpan = settings.get<number>('FileUpload_S3_URLExpiryTimeSpan');
const Region = settings.get<string>('FileUpload_S3_Region');
const SignatureVersion = settings.get<string>('FileUpload_S3_SignatureVersion');
// const SignatureVersion = settings.get<string>('FileUpload_S3_SignatureVersion');
const ForcePathStyle = settings.get<boolean>('FileUpload_S3_ForcePathStyle');
// const CDN = RocketChat.settings.get('FileUpload_S3_CDN');
const BucketURL = settings.get<string>('FileUpload_S3_BucketURL');
Expand All @@ -81,23 +81,23 @@ const configure = _.debounce(() => {

const config: Omit<S3Options, 'name' | 'getPath'> = {
connection: {
signatureVersion: SignatureVersion,
s3ForcePathStyle: ForcePathStyle,
params: {
Bucket,
ACL: Acl,
},
// signatureVersion: SignatureVersion,
forcePathStyle: ForcePathStyle,
region: Region,
followRegionRedirects: true,
},
params: {
Bucket,
ACL: Acl,
},
URLExpiryTimeSpan,
};

if (AWSAccessKeyId) {
config.connection.accessKeyId = AWSAccessKeyId;
}

if (AWSSecretAccessKey) {
config.connection.secretAccessKey = AWSSecretAccessKey;
if (AWSAccessKeyId && AWSSecretAccessKey) {
config.connection.credentials = {
accessKeyId: AWSAccessKeyId,
secretAccessKey: AWSSecretAccessKey,
};
}

if (BucketURL) {
Expand Down
Loading
Loading