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
6 changes: 6 additions & 0 deletions .changeset/app-action-button-role-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/apps-engine': minor
'@rocket.chat/meteor': minor
---

Accepts a role name in the `when.hasOneRole` and `when.hasAllRoles` filters of an app action button
52 changes: 49 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
node-version: ${{ steps.var.outputs.node-version }}
deno-version: ${{ steps.var.outputs.deno-version }}
source-hash: ${{ steps.source.outputs.hash }}
fips-skip: ${{ steps.fips.outputs.fips-skip }}
packages-build-cache-key: ${{ steps.ci-cache-keys.outputs.packages-build-cache-key }}
meteor-rc-cache-key: ${{ steps.ci-cache-keys.outputs.meteor-rc-cache-key }}
github-actions-changed: ${{ steps.diff.outputs.actions-changed }}
Expand Down Expand Up @@ -143,6 +144,43 @@ jobs:
echo "DOCKER_TAG: ${DOCKER_TAG}"
echo "gh-docker-tag=${DOCKER_TAG}" >> "$GITHUB_OUTPUT"

# How much of the FIPS suite to skip:
# all - build no fips images and run no fips tests
# ui-suite - build the images, run the api suites, skip the expensive UI shards (default)
# none - skip nothing, the full fips suite runs
# Forks skip 'all': the fips base image (rocketchat/dhi-node) sits in a private DockerHub
# repo and secrets are unavailable to forks, so the pull could never succeed.
- id: fips
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REPO: ${{ github.repository }}
PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
REPO_IS_FORK: ${{ github.event.repository.fork }}
HAS_FIPS_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'fips') }}
run: |
if [[ "$EVENT_NAME" == 'pull_request' ]]; then
if [[ "$PR_HEAD_REPO" == "$BASE_REPO" ]]; then
IS_FORK='false'
else
IS_FORK='true'
fi
else
IS_FORK="$REPO_IS_FORK"
fi

echo "EVENT_NAME: ${EVENT_NAME} / IS_FORK: ${IS_FORK}"

if [[ "$IS_FORK" == 'true' ]]; then
FIPS_SKIP='all'
elif [[ "$HAS_FIPS_LABEL" == 'true' ]]; then
FIPS_SKIP='none'
else
FIPS_SKIP='ui-suite'
fi

echo "FIPS_SKIP: ${FIPS_SKIP}"
echo "fips-skip=${FIPS_SKIP}" >> "$GITHUB_OUTPUT"

- id: diff
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down Expand Up @@ -365,6 +403,8 @@ jobs:
exclude:
- arch: arm64
type: fips
# drop the whole fips leg when there is nothing downstream to consume it (fork PRs)
- type: ${{ needs.release-versions.outputs.fips-skip == 'all' && 'fips' || '' }}
include:
- arch: amd64
service: [rocketchat]
Expand Down Expand Up @@ -763,6 +803,7 @@ jobs:

test-api-fips:
name: 🔨 Test API (FIPS)
if: needs.release-versions.outputs.fips-skip != 'all'
needs: [checks, build-gh-docker-publish, release-versions]

uses: ./.github/workflows/ci-test-e2e.yml
Expand All @@ -784,6 +825,7 @@ jobs:

test-api-livechat-fips:
name: 🔨 Test API Livechat (FIPS)
if: needs.release-versions.outputs.fips-skip != 'all'
needs: [checks, build-gh-docker-publish, release-versions]

uses: ./.github/workflows/ci-test-e2e.yml
Expand All @@ -805,7 +847,8 @@ jobs:

test-ui-fips:
name: 🔨 Test UI (FIPS)
if: contains(github.event.pull_request.labels.*.name, 'fips')
# only the 'fips' label opts a PR into the UI shards
if: needs.release-versions.outputs.fips-skip == 'none'
needs: [checks, build-gh-docker-publish, release-versions]
uses: ./.github/workflows/ci-test-e2e.yml
with:
Expand Down Expand Up @@ -1026,11 +1069,14 @@ jobs:
exit 1
fi

if [[ '${{ needs.test-api-fips.result }}' != 'success' ]]; then
# the fips jobs are gated by the release-versions 'fips-skip' output, so any of them
# can legitimately report 'skipped' (always on fork PRs, and test-ui-fips unless the
# PR carries the 'fips' label)
if [[ '${{ needs.test-api-fips.result }}' != 'success' && '${{ needs.test-api-fips.result }}' != 'skipped' ]]; then
exit 1
fi

if [[ '${{ needs.test-api-livechat-fips.result }}' != 'success' ]]; then
if [[ '${{ needs.test-api-livechat-fips.result }}' != 'success' && '${{ needs.test-api-livechat-fips.result }}' != 'skipped' ]]; then
exit 1
fi

Expand Down
67 changes: 67 additions & 0 deletions apps/meteor/client/hooks/useApplyButtonFilters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,73 @@ describe('useApplyButtonAuthFilter', () => {
});
});

describe('Custom role name resolution', () => {
// A custom role gets a random id, so an app can only know its name.
const customRole = { _id: 'aBcDeF1234567890x', name: 'Support Agent' };

const buttonRequiring = (role: string): IUIActionButton => ({
appId: 'test-app',
actionId: 'test-action',
labelI18n: 'test_label',
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
when: {
hasOneRole: [role],
},
});

it('should show button when the user holds the role given by name', () => {
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot().withJohnDoe().withRoleDefinition(customRole).withRole(customRole._id).build(),
});

expect(result.current(buttonRequiring(customRole.name))).toBe(true);
});

it('should filter button when the user does not hold the role given by name', () => {
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot()
.withJohnDoe({ roles: ['user'] })
.withRoleDefinition(customRole)
.build(),
});

expect(result.current(buttonRequiring(customRole.name))).toBe(false);
});

it('should still show button when the role is given by id', () => {
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot().withJohnDoe().withRoleDefinition(customRole).withRole(customRole._id).build(),
});

expect(result.current(buttonRequiring(customRole._id))).toBe(true);
});

it('should filter button when the role name is unknown', () => {
const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot().withJohnDoe().withRoleDefinition(customRole).withRole(customRole._id).build(),
});

expect(result.current(buttonRequiring('No Such Role'))).toBe(false);
});

it('should prefer the id over a role whose name collides with it', () => {
// `decoy.name` equals `customRole._id`. The user holds only the decoy, so
// resolving by name would wrongly show the button.
const decoy = { _id: 'zZyYxXw9876543210', name: customRole._id };

const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot()
.withJohnDoe({ roles: ['user'] })
.withRoleDefinition(customRole)
.withRoleDefinition(decoy)
.withRole(decoy._id)
.build(),
});

expect(result.current(buttonRequiring(customRole._id))).toBe(false);
});
});

describe('Permission-based filtering', () => {
it('should filter button when user does not have required permission (hasAllPermissions)', () => {
const button: IUIActionButton = {
Expand Down
12 changes: 8 additions & 4 deletions apps/meteor/client/hooks/useApplyButtonFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
isPublicDiscussion,
isPublicTeamRoom,
} from '@rocket.chat/core-typings';
import { AuthorizationContext, useUserId } from '@rocket.chat/ui-contexts';
import { AuthorizationContext, useRoleIdResolver, useUserId } from '@rocket.chat/ui-contexts';
import { useCallback, useContext } from 'react';

import { useRoom } from '../views/room/contexts/RoomContext';
Expand Down Expand Up @@ -69,17 +69,21 @@ export const useApplyButtonAuthFilter = (): ((button: IUIActionButton, room?: IR

const { queryAllPermissions, queryAtLeastOnePermission, queryRole } = useContext(AuthorizationContext);

// An app knows the name of a custom role, not the random id the workspace gave it,
// so accept either form in the role filters.
const resolveRoleId = useRoleIdResolver();

return useCallback(
(button: IUIActionButton, room?: IRoom) => {
const { hasAllPermissions, hasOnePermission, hasAllRoles, hasOneRole } = button.when || {};

const hasAllPermissionsResult = hasAllPermissions ? queryAllPermissions(hasAllPermissions)[1]() : true;
const hasOnePermissionResult = hasOnePermission ? queryAtLeastOnePermission(hasOnePermission)[1]() : true;
const hasAllRolesResult = hasAllRoles ? !!uid && hasAllRoles.every((role) => queryRole(role, room?._id)[1]()) : true;
const hasOneRoleResult = hasOneRole ? !!uid && hasOneRole.some((role) => queryRole(role, room?._id)[1]()) : true;
const hasAllRolesResult = hasAllRoles ? !!uid && hasAllRoles.every((role) => queryRole(resolveRoleId(role), room?._id)[1]()) : true;
const hasOneRoleResult = hasOneRole ? !!uid && hasOneRole.some((role) => queryRole(resolveRoleId(role), room?._id)[1]()) : true;

return hasAllPermissionsResult && hasOnePermissionResult && hasAllRolesResult && hasOneRoleResult;
},
[queryAllPermissions, queryAtLeastOnePermission, queryRole, uid],
[queryAllPermissions, queryAtLeastOnePermission, queryRole, resolveRoleId, uid],
);
};
2 changes: 1 addition & 1 deletion apps/meteor/ee/server/services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@rocket.chat/apps-engine": "workspace:^",
"@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/message-parser": "workspace:^",
"@rocket.chat/model-typings": "workspace:^",
"@rocket.chat/models": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"@rocket.chat/cron": "workspace:^",
"@rocket.chat/css-in-js": "^0.33.1",
"@rocket.chat/ddp-client": "workspace:^",
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/favicon": "workspace:^",
"@rocket.chat/federation-matrix": "workspace:^",
"@rocket.chat/federation-sdk": "0.7.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/uikit-playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"reactflow": "^11.11.4"
},
"devDependencies": {
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/tsconfig": "workspace:*",
"@types/lodash": "~4.17.25",
"@types/react": "~19.2.18",
Expand Down
2 changes: 1 addition & 1 deletion ee/apps/account-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"dependencies": {
"@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/logger": "workspace:^",
"@rocket.chat/model-typings": "workspace:^",
"@rocket.chat/models": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion ee/apps/authorization-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@rocket.chat/abac": "workspace:^",
"@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/logger": "workspace:^",
"@rocket.chat/model-typings": "workspace:^",
"@rocket.chat/models": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion ee/apps/ddp-streamer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"dependencies": {
"@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/instance-status": "workspace:^",
"@rocket.chat/logger": "workspace:^",
"@rocket.chat/model-typings": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion ee/apps/omnichannel-transcript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@react-pdf/renderer": "~4.5.1",
"@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/i18n": "workspace:^",
"@rocket.chat/logger": "workspace:^",
"@rocket.chat/model-typings": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion ee/apps/presence-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/cron": "workspace:^",
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/logger": "workspace:^",
"@rocket.chat/model-typings": "workspace:^",
"@rocket.chat/models": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion ee/apps/queue-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"dependencies": {
"@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/logger": "workspace:^",
"@rocket.chat/model-typings": "workspace:^",
"@rocket.chat/models": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion ee/packages/federation-matrix/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"dependencies": {
"@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/federation-sdk": "0.7.0",
"@rocket.chat/http-router": "workspace:^",
"@rocket.chat/license": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion ee/packages/media-calls/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
"dependencies": {
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/logger": "workspace:^",
"@rocket.chat/media-signaling": "workspace:^",
"@rocket.chat/models": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion ee/packages/omnichannel-services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"dependencies": {
"@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/i18n": "workspace:^",
"@rocket.chat/logger": "workspace:^",
"@rocket.chat/message-types": "workspace:~",
Expand Down
5 changes: 2 additions & 3 deletions fuselage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,7 @@ if [[ $action == "next-all" || $action == "latest-all" ]]; then
targetVersion="latest"
fi

echo "📦 @rocket.chat/emitter [UPDATING to $targetVersion version...]
📦 @rocket.chat/fuselage-toastbar [UPDATING to $targetVersion version...]
echo "📦 @rocket.chat/fuselage-toastbar [UPDATING to $targetVersion version...]
📦 @rocket.chat/fuselage-tokens [UPDATING to $targetVersion version...]
📦 @rocket.chat/css-in-js [UPDATING to $targetVersion version...]
📦 @rocket.chat/styled [UPDATING to $targetVersion version...]
Expand All @@ -122,7 +121,7 @@ if [[ $action == "next-all" || $action == "latest-all" ]]; then
📦 @rocket.chat/onboarding-ui [UPDATING to $targetVersion version...]
📦 @rocket.chat/layout [UPDATING to $targetVersion version...]"

eval "yarn up @rocket.chat/emitter@$targetVersion @rocket.chat/fuselage-toastbar@$targetVersion @rocket.chat/fuselage-tokens@$targetVersion @rocket.chat/css-in-js@$targetVersion @rocket.chat/styled@$targetVersion @rocket.chat/fuselage@$targetVersion @rocket.chat/fuselage-hooks@$targetVersion @rocket.chat/icons@$targetVersion @rocket.chat/logo@$targetVersion @rocket.chat/memo@$targetVersion @rocket.chat/onboarding-ui@$targetVersion @rocket.chat/layout@$targetVersion"
eval "yarn up @rocket.chat/fuselage-toastbar@$targetVersion @rocket.chat/fuselage-tokens@$targetVersion @rocket.chat/css-in-js@$targetVersion @rocket.chat/styled@$targetVersion @rocket.chat/fuselage@$targetVersion @rocket.chat/fuselage-hooks@$targetVersion @rocket.chat/icons@$targetVersion @rocket.chat/logo@$targetVersion @rocket.chat/memo@$targetVersion @rocket.chat/onboarding-ui@$targetVersion @rocket.chat/layout@$targetVersion"
exit 1
fi

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,29 @@ export interface IUActionButtonWhen {
messageActionContext?: Array<MessageActionContext>;
hasOnePermission?: Array<string>;
hasAllPermissions?: Array<string>;
/**
* Show the button when the user holds at least one of these roles.
*
* Each entry is a role id or a role name. Prefer the name for a custom role,
* because its id differs between workspaces.
*
* A role scoped to `Subscriptions` — `owner`, `moderator`, `leader`, or a custom
* one — is granted per room, so it matches only on surfaces bound to a room. On a
* surface with no room of its own, the user dropdown for instance, only roles
* scoped to `Users` match.
*/
hasOneRole?: Array<string>;
/**
* Show the button when the user holds every one of these roles.
*
* Each entry is a role id or a role name. Prefer the name for a custom role,
* because its id differs between workspaces.
*
* A role scoped to `Subscriptions` — `owner`, `moderator`, `leader`, or a custom
* one — is granted per room, so it matches only on surfaces bound to a room. On a
* surface with no room of its own, the user dropdown for instance, only roles
* scoped to `Users` match.
*/
hasAllRoles?: Array<string>;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/ddp-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"ws": "~8.21.3"
},
"peerDependencies": {
"@rocket.chat/emitter": "*"
"@rocket.chat/emitter": "workspace:~"
},
"volta": {
"extends": "../../package.json"
Expand Down
Loading
Loading