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
41 changes: 41 additions & 0 deletions api/v4/source/access_control.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -675,3 +675,44 @@
$ref: "#/components/responses/InternalServerError"
"501":
$ref: "#/components/responses/NotImplemented"
/api/v4/access_control/decisions/actions/search:
post:
tags:
- access control
summary: Search allowed actions for the current user (render-time decision)
description: |
Returns non-authoritative, render-time ABAC decisions for the current
session user on a given resource and set of actions. Use these decisions
to decide whether to show or hide UI controls before the user attempts
an action.

The subject is always the authenticated session user; there is no way to
probe another user's decisions. Server-side enforcement always re-evaluates
the full policy on the actual request and remains the source of truth.

Gated by the `PermissionPolicies` feature flag and an Enterprise license.
When ABAC is inactive for the resource, every action is returned as
`allowed: true, evaluated: true`.

##### Permissions
Must be authenticated (any logged-in user).
operationId: SearchAccessControlDecisionActions
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ActionSearchRequest"
responses:
"200":
description: Render-time action decisions returned successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/ActionSearchResponse"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalServerError"
114 changes: 114 additions & 0 deletions api/v4/source/definitions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5446,6 +5446,120 @@ components:
type: integer
format: int64
description: The time in milliseconds the recap channel was created
AccessControlResource:
type: object
description: Identifies a resource (type + id) for an ABAC decision request.
required:
- type
- id
properties:
type:
type: string
description: The resource type (e.g. "channel").
id:
type: string
description: The resource identifier.
RenderPermissionDecision:
type: object
description: |
A non-authoritative, render-time ABAC decision for a single action.
Must not be used to authorize an action — server enforcement is authoritative.
required:
- allowed
- evaluated
properties:
allowed:
type: boolean
description: Whether the action is permitted for rendering purposes.
evaluated:
type: boolean
description: Whether the server intentionally computed this decision.
reason:
type: string
description: Generic denial reason (e.g. "restricted_by_policy"). Never contains policy names, expressions, or attribute values.
ActionSearchResult:
type: object
required:
- action
properties:
action:
type: object
required:
- name
properties:
name:
type: string
description: Name of a permitted action. Denial is expressed by omission from the results list.
ActionSearchSubject:
type: object
description: >
RESERVED for Phase 3 cross-subject evaluation. In Phase 2 the ID must equal
the authenticated session user ID; mismatches return 403. Present to allow
Phase 3 as a non-breaking extension.
required:
- id
properties:
id:
type: string
type:
type: string
ActionSearchPage:
type: object
description: >
RESERVED for Phase 3 pagination. Accepted in requests but always ignored;
next_token is never emitted in responses. Present to allow Phase 3
pagination as a non-breaking extension.
properties:
next_token:
type: string
ActionSearchRequest:
type: object
description: Request body for the Action Search render-decision endpoint.
required:
- resource
properties:
resource:
$ref: "#/components/schemas/AccessControlResource"
actions:
type: array
description: >
Actions to evaluate (max 16). Omit or leave empty for discovery mode:
the server evaluates all registered actions for the resource type and
returns the permitted set.
maxItems: 16
items:
type: string
subject:
$ref: "#/components/schemas/ActionSearchSubject"
page:
$ref: "#/components/schemas/ActionSearchPage"
ActionSearchResponse:
type: object
description: Response from the Action Search render-decision endpoint.
required:
- resource
- results
- decisions
properties:
resource:
$ref: "#/components/schemas/AccessControlResource"
results:
type: array
description: >
AuthZEN-canonical list: only PERMITTED actions appear here.
Denial is expressed by omission. Always present; an empty array []
is meaningful (all evaluated actions were denied).
items:
$ref: "#/components/schemas/ActionSearchResult"
decisions:
type: object
description: >
Mattermost extension: all evaluated actions with full decision detail
(allowed, evaluated, reason), including denied ones. Always present.
additionalProperties:
$ref: "#/components/schemas/RenderPermissionDecision"
page:
$ref: "#/components/schemas/ActionSearchPage"
RecapLimitStatus:
type: object
description: The current user's recap limit status including usage and cooldown information
Expand Down
2 changes: 2 additions & 0 deletions e2e-tests/playwright/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export {setWysiwygUserPreference, WYSIWYG_PREF_CATEGORY, WYSIWYG_PREF_NAME} from

export {TextInputSetting} from './ui/components/system_console/base_components';

export {expectFilesVisible, expectFilesRedacted} from './ui/components/channels/post';

export {TestArgs, ScreenshotOptions} from './types';

export {
Expand Down
10 changes: 10 additions & 0 deletions e2e-tests/playwright/lib/src/ui/components/channels/center_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ export default class ChannelsCenterView {
return new ChannelsPost(lastPost);
}

/**
* Return the Center post whose body contains the given text. Prefer this over getLastPost:
* adding a member appends a join system message, so the post under test is often not last.
*/
async getPostByText(text: string) {
const post = this.container.getByTestId('postView').filter({hasText: text}).last();
await post.waitFor();
return new ChannelsPost(post);
}

/**
* Return the ID of the last post in the Center
*/
Expand Down
39 changes: 39 additions & 0 deletions e2e-tests/playwright/lib/src/ui/components/channels/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ import BurnOnReadTimerChip from './burn_on_read_timer_chip';
import PostMenu from './post_menu';
import ThreadFooter from './thread_footer';

// Both assert the positive case first: a lone "placeholder is absent" check also passes
// against a region that has not rendered at all.
export async function expectFilesVisible(scope: Locator) {
await expect(scope.getByTestId('fileAttachmentList')).toBeVisible();
await expect(scope.getByTestId('redactedFilesPlaceholder')).toHaveCount(0);
}

export async function expectFilesRedacted(scope: Locator) {
await expect(scope.getByTestId('redactedFilesPlaceholder')).toBeVisible();
await expect(scope.getByTestId('fileAttachmentList')).toHaveCount(0);
}

export default class ChannelsPost {
readonly container: Locator;

Expand All @@ -29,6 +41,12 @@ export default class ChannelsPost {
readonly burnOnReadTimerChip;
readonly concealedPlaceholder;

// File attachments and their ABAC-redacted stand-in
readonly fileAttachmentList;
readonly redactedFilesPlaceholder;

readonly postPreview;

constructor(container: Locator) {
this.container = container;

Expand All @@ -50,6 +68,13 @@ export default class ChannelsPost {
this.concealedPlaceholder = new BurnOnReadConcealedPlaceholder(
container.getByTestId(/^burn-on-read-concealed-/),
);

this.fileAttachmentList = container.getByTestId('fileAttachmentList');
this.redactedFilesPlaceholder = container.getByTestId('redactedFilesPlaceholder');

// The embedded permalink preview carries no test id, so the class name is the
// only handle available.
this.postPreview = container.locator('.post-preview');
}

async toBeVisible() {
Expand Down Expand Up @@ -143,6 +168,20 @@ export default class ChannelsPost {
await expect(this.container).not.toContainText(text);
}

/**
* @param scope Sub-region to assert within, e.g. an embedded permalink preview
*/
async toHaveFilesVisible(scope: Locator = this.container) {
await expectFilesVisible(scope);
}

/**
* @param scope Sub-region to assert within, e.g. an embedded permalink preview
*/
async toHaveFilesRedacted(scope: Locator = this.container) {
await expectFilesRedacted(scope);
}

/**
* Check if this is a burn-on-read post
*/
Expand Down
Loading
Loading