Skip to content

Commit

Permalink
Merge branch 'develop' into fix/slack-csv-import
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] authored Aug 24, 2022
2 parents 630b6ae + c3eb54a commit 46af985
Show file tree
Hide file tree
Showing 547 changed files with 12,523 additions and 7,288 deletions.
15 changes: 8 additions & 7 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@ jobs:
- name: Start containers
env:
MONGO_URL: "mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true"
MONGO_OPLOG_URL: "mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true"
MONGO_URL: 'mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true'
MONGO_OPLOG_URL: 'mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true'
run: |
export LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]")
Expand Down Expand Up @@ -440,11 +440,12 @@ jobs:
- name: Start containers
env:
MONGO_URL: "mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true"
RC_DOCKERFILE: "${{ github.workspace }}/apps/meteor/.docker/Dockerfile"
RC_DOCKER_TAG: "${{ needs.release-versions.outputs.gh-docker-tag }}.official"
MONGO_URL: 'mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true'
RC_DOCKERFILE: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile'
RC_DOCKER_TAG: '${{ needs.release-versions.outputs.gh-docker-tag }}.official'
DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }}
TRANSPORTER: nats://nats:4222
ENTERPRISE_LICENSE: ${{ secrets.ENTERPRISE_LICENSE }}
run: |
export LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]")
Expand Down Expand Up @@ -493,7 +494,7 @@ jobs:
cd ./apps/meteor
for i in $(seq 1 5); do
npm run testapi && s=0 && break || s=$?;
IS_EE=true npm run testapi && s=0 && break || s=$?;
docker compose -f ../../docker-compose-ci.yml logs --tail=100
Expand Down Expand Up @@ -759,7 +760,7 @@ jobs:
run: |
LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]")
GH_IMAGE_NAME="ghcr.io/${LOWERCASE_REPOSITORY}/${{ matrix.service }}-service:${{ needs.release-versions.outputs.gh-docker-tag }}.${{ matrix.release }}"
GH_IMAGE_NAME="ghcr.io/${LOWERCASE_REPOSITORY}/${{ matrix.service }}-service:${{ needs.release-versions.outputs.gh-docker-tag }}"
echo "GH_IMAGE_NAME: $GH_IMAGE_NAME"
Expand Down
10 changes: 8 additions & 2 deletions apps/meteor/app/api/server/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,23 @@ type NotFoundResult = {
};
};

export type TOperation = 'hasAll' | 'hasAny';
export type NonEnterpriseTwoFactorOptions = {
authRequired: true;
forceTwoFactorAuthenticationForNonEnterprise: true;
twoFactorRequired: true;
permissionsRequired?: string[];
permissionsRequired?: string[] | { [key in Method]: string[] } | { [key in Method]: { operation: TOperation; permissions: string[] } };
twoFactorOptions: ITwoFactorOptions;
};

type Options = (
| {
permissionsRequired?: string[];
permissionsRequired?:
| string[]
| ({ [key in Method]?: string[] } & { '*'?: string[] })
| ({ [key in Method]?: { operation: TOperation; permissions: string[] } } & {
'*'?: { operation: TOperation; permissions: string[] };
});
authRequired?: boolean;
forceTwoFactorAuthenticationForNonEnterprise?: boolean;
}
Expand Down
103 changes: 103 additions & 0 deletions apps/meteor/app/api/server/api.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { IUser } from '@rocket.chat/core-typings';

import { hasAllPermissionAsync, hasAtLeastOnePermissionAsync } from '../../authorization/server/functions/hasPermission';

type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | '*';
export type PermissionsPayload = {
[key in RequestMethod]?: {
operation: 'hasAll' | 'hasAny';
permissions: string[];
};
};

export type PermissionsPayloadLight = {
[key in RequestMethod]?: string[];
};

export type PermissionsRequiredKey = string[] | PermissionsPayload | PermissionsPayloadLight;

const isLegacyPermissionsPayload = (permissionsPayload: PermissionsRequiredKey): permissionsPayload is string[] => {
return Array.isArray(permissionsPayload);
};

const isLightPermissionsPayload = (permissionsPayload: PermissionsRequiredKey): permissionsPayload is PermissionsPayloadLight => {
return (
typeof permissionsPayload === 'object' &&
Object.keys(permissionsPayload).some((key) => ['GET', 'POST', 'PUT', 'DELETE', '*'].includes(key.toUpperCase())) &&
Object.values(permissionsPayload).every((value) => Array.isArray(value))
);
};

const isPermissionsPayload = (permissionsPayload: PermissionsRequiredKey): permissionsPayload is PermissionsPayload => {
return (
typeof permissionsPayload === 'object' &&
Object.keys(permissionsPayload).some((key) => ['GET', 'POST', 'PUT', 'DELETE', '*'].includes(key.toUpperCase())) &&
Object.values(permissionsPayload).every((value) => typeof value === 'object' && value.operation && value.permissions)
);
};

export async function checkPermissionsForInvocation(
userId: IUser['_id'],
permissionsPayload: PermissionsPayload,
requestMethod: RequestMethod,
): Promise<boolean> {
const permissions = permissionsPayload[requestMethod] || permissionsPayload['*'];

if (!permissions) {
// how we reached here in the first place?
return false;
}

if (permissions.permissions.length === 0) {
// You can pass an empty array of permissions to allow access to the method
return true;
}

if (permissions.operation === 'hasAll') {
return hasAllPermissionAsync(userId, permissions.permissions);
}

if (permissions.operation === 'hasAny') {
return hasAtLeastOnePermissionAsync(userId, permissions.permissions);
}

return false;
}

// We'll assume options only contains permissionsRequired, as we don't care of the other elements
export function checkPermissions(options: { permissionsRequired: PermissionsRequiredKey }) {
if (!options.permissionsRequired) {
return false;
}

if (isPermissionsPayload(options.permissionsRequired)) {
// No modifications needed
return true;
}

if (isLegacyPermissionsPayload(options.permissionsRequired)) {
options.permissionsRequired = {
'*': {
operation: 'hasAll',
permissions: options.permissionsRequired,
},
};
return true;
}

if (isLightPermissionsPayload(options.permissionsRequired)) {
Object.keys(options.permissionsRequired).forEach((method) => {
const methodKey = method as RequestMethod;
// @ts-expect-error -- we know the type of the value but ts refuses to infer it
options.permissionsRequired[methodKey] = {
operation: 'hasAll',
// @ts-expect-error -- we know the type of the value but ts refuses to infer it
permissions: options.permissionsRequired[methodKey],
};
});
return true;
}

// If reached here, options.permissionsRequired contained an invalid payload
return false;
}
19 changes: 8 additions & 11 deletions apps/meteor/app/api/server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { Logger } from '../../../server/lib/logger/Logger';
import { getRestPayload } from '../../../server/lib/logger/logPayloads';
import { settings } from '../../settings/server';
import { metrics } from '../../metrics/server';
import { hasPermission, hasAllPermission } from '../../authorization/server';
import { hasPermission } from '../../authorization/server';
import { getDefaultUserFields } from '../../utils/server/functions/getDefaultUserFields';
import { checkCodeForUser } from '../../2fa/server/code';
import { checkPermissionsForInvocation, checkPermissions } from './api.helpers';

const logger = new Logger('API');

Expand Down Expand Up @@ -318,14 +319,7 @@ export class APIClass extends Restivus {
options = {};
}

let shouldVerifyPermissions;

if (!_.isArray(options.permissionsRequired)) {
options.permissionsRequired = undefined;
shouldVerifyPermissions = false;
} else {
shouldVerifyPermissions = !!options.permissionsRequired.length;
}
const shouldVerifyPermissions = checkPermissions(options);

// Allow for more than one route using the same option and endpoints
if (!_.isArray(routes)) {
Expand Down Expand Up @@ -434,8 +428,11 @@ export class APIClass extends Restivus {
throw new Meteor.Error('invalid-params', validatorFunc.errors?.map((error) => error.message).join('\n '));
}
}

if (shouldVerifyPermissions && (!this.userId || !hasAllPermission(this.userId, _options.permissionsRequired))) {
if (
shouldVerifyPermissions &&
(!this.userId ||
!Promise.await(checkPermissionsForInvocation(this.userId, _options.permissionsRequired, this.request.method)))
) {
throw new Meteor.Error('error-unauthorized', 'User does not have the permissions required for this action', {
permissions: _options.permissionsRequired,
});
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/lib/getServerInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP

type ServerInfo =
| {
info: Info;
info: typeof Info;
}
| {
version: string | undefined;
Expand Down
37 changes: 22 additions & 15 deletions apps/meteor/app/api/server/v1/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ import {
import { API } from '../api';
import { Imports } from '../../../models/server';
import { Importers } from '../../../importer/server';
import { executeUploadImportFile } from '../../../importer/server/methods/uploadImportFile';
import {
executeUploadImportFile,
executeDownloadPublicImportFile,
executeGetImportProgress,
executeGetImportFileData,
executeStartImport,
executeGetLatestImportOperations,
} from '../../../importer/server/methods';

API.v1.addRoute(
'uploadImportFile',
Expand All @@ -36,12 +43,12 @@ API.v1.addRoute(
{
authRequired: true,
validateParams: isDownloadPublicImportFileParamsPOST,
permissionsRequired: ['run-import'],
},
{
post() {
const { fileUrl, importerKey } = this.bodyParams;

Meteor.call('downloadPublicImportFile', fileUrl, importerKey);
executeDownloadPublicImportFile(this.userId, fileUrl, importerKey);

return API.v1.success();
},
Expand All @@ -53,12 +60,13 @@ API.v1.addRoute(
{
authRequired: true,
validateParams: isStartImportParamsPOST,
permissionsRequired: ['run-import'],
},
{
post() {
const { input } = this.bodyParams;

Meteor.call('startImport', input);
executeStartImport({ input });

return API.v1.success();
},
Expand All @@ -70,10 +78,12 @@ API.v1.addRoute(
{
authRequired: true,
validateParams: isGetImportFileDataParamsGET,
permissionsRequired: ['run-import'],
},
{
get() {
const result = Meteor.call('getImportFileData');
async get() {
const result = await executeGetImportFileData();

return API.v1.success(result);
},
},
Expand All @@ -84,10 +94,11 @@ API.v1.addRoute(
{
authRequired: true,
validateParams: isGetImportProgressParamsGET,
permissionsRequired: ['run-import'],
},
{
get() {
const result = Meteor.call('getImportProgress');
const result = executeGetImportProgress();
return API.v1.success(result);
},
},
Expand All @@ -98,10 +109,11 @@ API.v1.addRoute(
{
authRequired: true,
validateParams: isGetLatestImportOperationsParamsGET,
permissionsRequired: ['view-import-operations'],
},
{
get() {
const result = Meteor.call('getLatestImportOperations');
const result = executeGetLatestImportOperations();
return API.v1.success(result);
},
},
Expand All @@ -118,9 +130,7 @@ API.v1.addRoute(
post() {
const importer = Importers.get('pending-files');
if (!importer) {
throw new Meteor.Error('error-importer-not-defined', 'The Pending File Importer was not found.', {
method: 'downloadPendingFiles',
});
throw new Meteor.Error('error-importer-not-defined', 'The Pending File Importer was not found.', 'downloadPendingFiles');
}

importer.instance = new importer.importer(importer); // eslint-disable-line new-cap
Expand All @@ -144,9 +154,7 @@ API.v1.addRoute(
post() {
const importer = Importers.get('pending-avatars');
if (!importer) {
throw new Meteor.Error('error-importer-not-defined', 'The Pending File Importer was not found.', {
method: 'downloadPendingAvatars',
});
throw new Meteor.Error('error-importer-not-defined', 'The Pending File Importer was not found.', 'downloadPendingAvatars');
}

importer.instance = new importer.importer(importer); // eslint-disable-line new-cap
Expand All @@ -170,7 +178,6 @@ API.v1.addRoute(
get() {
const operation = Imports.findLastImport();
return API.v1.success({
success: true,
operation,
});
},
Expand Down
39 changes: 39 additions & 0 deletions apps/meteor/app/api/server/v1/webdav.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import Ajv from 'ajv';

import { API } from '../api';
import { findWebdavAccountsByUserId } from '../lib/webdav';

// TO-DO: remove this AJV instance and import one from the core-typings
const ajv = new Ajv({ coerceTypes: true });

type POSTRemoveWebdavAccount = {
accountId: string;
};

const POSTRemoveWebdavAccountSchema = {
type: 'object',
properties: {
accountId: {
type: 'string',
},
},
required: ['accountId'],
additionalProperties: false,
};

export const isPOSTRemoveWebdavAccount = ajv.compile<POSTRemoveWebdavAccount>(POSTRemoveWebdavAccountSchema);

API.v1.addRoute(
'webdav.getMyAccounts',
{ authRequired: true },
Expand All @@ -12,3 +34,20 @@ API.v1.addRoute(
},
},
);

API.v1.addRoute(
'webdav.removeWebdavAccount',
{
authRequired: true,
validateParams: isPOSTRemoveWebdavAccount,
},
{
async post() {
const { accountId } = this.bodyParams;

const result = Meteor.call('removeWebdavAccount', accountId);

return API.v1.success({ result });
},
},
);
Loading

0 comments on commit 46af985

Please sign in to comment.