Skip to content

Commit

Permalink
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into newT…
Browse files Browse the repository at this point in the history
…hreads

* 'develop' of github.com:RocketChat/Rocket.Chat: (93 commits)
  Chore: Upgrade dependencies (#26694)
  Chore: More Omnichannel tests (#26691)
  Regression: Banner - Room not found - Omnichannel room (#26693)
  [NEW] Capability to search visitors by custom fields (#26312)
  Chore: Create tests for Omnichannel admin add a custom fields (#26609)
  [FIX] Avatars of other chats disappear when they located near chat with broken avatar (#26689)
  [IMPROVE] Added identification on calls to/from existing contacts (#26334)
  Regression: invalid statistics format  (#26684)
  Regression: "Cache size is not a function" error when booting (#26683)
  [FIX] Correct IMAP configuration for email inbox (#25789)
  [FIX] Active users count on `@all` and `@here`  (#25957)
  [FIX] Autotranslate method should respect setting (#26549)
  Chore: Remove italic/bold font-style from system messages (#26655)
  Chore: Convert AppSetting to tsx (#26625)
  Chore: Remove & Test old closeChat templates (#26631)
  [IMPROVE] General federation improvements (#26150)
  [NEW] Warn admins about running multiple instances of the monolith (#26667)
  Regression: Prevent message from being temp forever (#26668)
  Regression: Add alsoSendThreadToChannel to user settings api (#26663)
  [IMPROVE] Spotlight search user results (#26599)
  ...
  • Loading branch information
gabriellsh committed Aug 26, 2022
2 parents 54a9483 + b2480ad commit f1e2793
Show file tree
Hide file tree
Showing 992 changed files with 24,436 additions and 14,412 deletions.
13 changes: 13 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,16 @@
/apps/meteor/client/ @RocketChat/frontend
/apps/meteor/tests/ @RocketChat/chat-engine
/apps/meteor/app/apps/ @RocketChat/apps
/apps/meteor/app/livechat @RocketChat/omnichannel
/apps/meteor/app/voip @RocketChat/omnichannel
/apps/meteor/app/sms @RocketChat/omnichannel
/apps/meteor/packages/rocketchat-livechat @RocketChat/omnichannel
/apps/meteor/server/services/voip @RocketChat/omnichannel
/apps/meteor/server/services/omnichannel-voip @RocketChat/omnichannel
/apps/meteor/server/features/EmailInbox @RocketChat/omnichannel
/apps/meteor/ee/app/canned-responses @RocketChat/omnichannel
/apps/meteor/ee/app/livechat @RocketChat/omnichannel
/apps/meteor/ee/app/livechat-enterprise @RocketChat/omnichannel
/apps/meteor/ee/client/omnichannel @RocketChat/omnichannel
/apps/meteor/client/components/omnichannel @RocketChat/omnichannel
/apps/meteor/client/components/voip @RocketChat/omnichannel
46 changes: 29 additions & 17 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 @@ -754,34 +755,45 @@ jobs:
username: ${{ secrets.CR_USER }}
password: ${{ secrets.CR_PAT }}

- name: Publish Docker images
- name: Get Docker image name
id: gh-docker
run: |
LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]")
IMAGE_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}"
GH_IMAGE_NAME="ghcr.io/${LOWERCASE_REPOSITORY}/${{ matrix.service }}-service:${IMAGE_TAG}"
GH_IMAGE_NAME="ghcr.io/${LOWERCASE_REPOSITORY}/${{ matrix.service }}-service:${{ needs.release-versions.outputs.gh-docker-tag }}"
echo "GH_IMAGE_NAME: $GH_IMAGE_NAME"
docker pull $GH_IMAGE_NAME
echo "::set-output name=gh-image-name::${GH_IMAGE_NAME}"
- name: Pull Docker image
run: docker pull ${{ steps.gh-docker.outputs.gh-image-name }}

- name: Publish Docker images
run: |
DH_IMAGE_NAME="rocketchat/${{ matrix.service }}-service"
# 'develop' or 'tag'
DOCKER_TAG=$GITHUB_REF_NAME
docker tag $GH_IMAGE_NAME rocketchat/${{ matrix.service }}-service:${IMAGE_TAG}
docker push rocketchat/${{ matrix.service }}-service:${IMAGE_TAG}
echo "DH_IMAGE_NAME: $DH_IMAGE_NAME"
echo "DOCKER_TAG: $DOCKER_TAG"
# tag and push the specific tag version
docker tag ${{ steps.gh-docker.outputs.gh-image-name }} $DH_IMAGE_NAME:$DOCKER_TAG
docker push $DH_IMAGE_NAME:$DOCKER_TAG
if [[ $GITHUB_REF == refs/tags/* ]]; then
RELEASE="${{ needs.release-versions.outputs.release }}"
echo "RELEASE: $RELEASE"
if [[ $RELEASE == 'latest' ]]; then
if [[ '${{ needs.release-versions.outputs.latest-release }}' == $GITHUB_REF_NAME ]]; then
docker tag rocketchat/${{ matrix.service }}-service:${IMAGE_TAG} rocketchat/${{ matrix.service }}-service:${RELEASE}
docker push rocketchat/${{ matrix.service }}-service:${RELEASE}
docker tag ${{ steps.gh-docker.outputs.gh-image-name }} $DH_IMAGE_NAME:$RELEASE
docker push $DH_IMAGE_NAME:$RELEASE
fi
else
docker tag rocketchat/${{ matrix.service }}-service:${IMAGE_TAG} rocketchat/${{ matrix.service }}-service:${RELEASE}
docker push rocketchat/${{ matrix.service }}-service:${RELEASE}
docker tag ${{ steps.gh-docker.outputs.gh-image-name }} $DH_IMAGE_NAME:$RELEASE
docker push $DH_IMAGE_NAME:$RELEASE
fi
fi
7 changes: 6 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@
}
],
"typescript.tsdk": "./node_modules/typescript/lib",
"cSpell.words": ["photoswipe", "tmid"]
"cSpell.words": [
"livechat",
"omnichannel",
"photoswipe",
"tmid"
]
}
8 changes: 1 addition & 7 deletions apps/meteor/app/2fa/client/overrideMeteorCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Meteor } from 'meteor/meteor';
import { t } from '../../utils/client';
import { process2faReturn } from '../../../client/lib/2fa/process2faReturn';
import { isTotpInvalidError } from '../../../client/lib/2fa/utils';
import { dispatchToastMessage } from '../../../client/lib/toast';

const { call } = Meteor;

Expand All @@ -17,12 +16,7 @@ const callWithTotp =
(twoFactorCode: string, twoFactorMethod: string): unknown =>
call(methodName, ...args, { twoFactorCode, twoFactorMethod }, (error: unknown, result: unknown): void => {
if (isTotpInvalidError(error)) {
(error as { toastrShowed?: true }).toastrShowed = true;
dispatchToastMessage({
type: 'error',
message: twoFactorMethod === 'password' ? t('Invalid_password') : t('Invalid_two_factor_code'),
});
callback(error);
callback(new Error(twoFactorMethod === 'password' ? t('Invalid_password') : t('Invalid_two_factor_code')));
return;
}

Expand Down
8 changes: 4 additions & 4 deletions apps/meteor/app/action-links/client/lib/actionLinks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import type { IMessage } from '@rocket.chat/core-typings';

import { handleError } from '../../../../client/lib/utils/handleError';
import { dispatchToastMessage } from '../../../../client/lib/toast';

// Action Links namespace creation.
export const actionLinks = {
Expand Down Expand Up @@ -71,9 +71,9 @@ export const actionLinks = {
}

// and run on server side
Meteor.call('actionLinkHandler', name, message._id, (err: Error) => {
if (err && !ranClient) {
handleError(err);
Meteor.call('actionLinkHandler', name, message._id, (error: unknown) => {
if (error && !ranClient) {
dispatchToastMessage({ type: 'error', message: error });
}
});
},
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
20 changes: 11 additions & 9 deletions apps/meteor/app/api/server/v1/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,25 @@ API.v1.addRoute(
{ authRequired: true },
{
async post() {
const [asset, { refreshAllClients }, assetName] = await getUploadFormData({
request: this.request,
});
const [asset, { refreshAllClients, assetName: customName }, fileName] = await getUploadFormData(
{
request: this.request,
},
{ field: 'asset' },
);

const assetName = customName || fileName;
const assetsKeys = Object.keys(RocketChatAssets.assets);

const isValidAsset = assetsKeys.includes(assetName);
if (!isValidAsset) {
throw new Meteor.Error('error-invalid-asset', 'Invalid asset');
}

Meteor.runAsUser(this.userId, () => {
Meteor.call('setAsset', asset.fileBuffer, asset.mimetype, assetName);
if (refreshAllClients) {
Meteor.call('refreshClients');
}
});
Meteor.call('setAsset', asset.fileBuffer, asset.mimetype, assetName);
if (refreshAllClients) {
Meteor.call('refreshClients');
}

return API.v1.success();
},
Expand Down
Loading

0 comments on commit f1e2793

Please sign in to comment.