mattermost-mobile-upstream/detox/e2e/support/server_api/access_control.ts
yasser khan fca832c684
Some checks are pending
ci / lint-typecheck (push) Waiting to run
ci / detox-lint-typecheck (push) Waiting to run
ci / test (push) Waiting to run
fix(e2e): stabilize CMT and main Detox/Maestro E2E flakes (#10301)
* fix(e2e): preserve iOS App Group container metadata across shard specs and use swipe helper in Edit Server spec

* fix(maestro): dismiss iOS QuickPath keyboard onboarding and retry empty server URL input

* fix(e2e): avoid replaying single-use FormData streams on HTML interstitials and add ABAC/login transport retries

* fix(e2e): classify disabled-feature and server-timeout errors as infra in CMT notifications

* fix(e2e): address CodeRabbit review on container cache, ABAC cleanup and Maestro connect scroll

* fix(e2e): wait for restored denied state in upload permission spec as well

* fix(e2e): confirm restored ABAC decision server-side and scroll to URL label before Maestro recovery

* fix(e2e): return to channel list before ABAC cleanup logout and drop optional flag on recovery scroll

* fix(e2e): retry ABAC decision login on transport failures and classify CF 521/523 as transport failures

* test(e2e): cover apiUploadFileToChannel rebuilding FormData and stream on interstitial retry

* fix(e2e): budget ABAC decision waits for observed server flip latency

Co-authored-by: yasser khan <attitude3cena.yf@gmail.com>
2026-11-04 13:07:51 +05:30

302 lines
11 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {timeouts} from '@support/utils';
import {withTransportRetry} from '@support/utils/transport_retry';
import client from './client';
import {getResponseFromError} from './common';
// ****************************************************************
// Access Control (ABAC)
// See https://api.mattermost.com/#tag/access-control
//
// Exported API function should have the following:
// - documented using JSDoc
// - meaningful description
// - match the referenced API endpoints
// - parameter/s defined by `@param`
// - return value defined by `@return`
// ****************************************************************
// ------------------------------------------------------------
// ABAC Config
// ------------------------------------------------------------
/**
* Enable Attribute-Based Access Control (ABAC) via server config.
* Requires admin session.
* @param {string} baseUrl - the base server URL
* @return {Object} returns {config} on success or {error, status} on error
*/
export const apiEnableABAC = async (baseUrl: string): Promise<any> => {
try {
const response = await client.put(`${baseUrl}/api/v4/config/patch`, {
AccessControlSettings: {
EnableAttributeBasedAccessControl: true,
},
});
return {config: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
// ------------------------------------------------------------
// Custom Profile Attribute Fields
// Used to define user attributes that ABAC policies can reference.
// See /api/v4/custom_profile_attributes/fields
// ------------------------------------------------------------
/**
* List all custom profile attribute fields.
* @param {string} baseUrl - the base server URL
* @return {Object} returns {fields} on success or {error, status} on error
*/
export const apiGetCustomProfileAttributeFields = async (baseUrl: string): Promise<any> => {
return withTransportRetry(async () => {
try {
const response = await client.get(`${baseUrl}/api/v4/custom_profile_attributes/fields`);
return {fields: response.data ?? []};
} catch (err) {
return getResponseFromError(err);
}
}, {idempotent: true, label: 'apiGetCustomProfileAttributeFields', budgetMs: timeouts.ONE_MIN});
};
/**
* Delete a custom profile attribute field by ID.
* @param {string} baseUrl - the base server URL
* @param {string} fieldId - the field ID to delete
* @return {Object} returns {} on success or {error, status} on error
*/
export const apiDeleteCustomProfileAttributeField = async (baseUrl: string, fieldId: string): Promise<any> => {
return withTransportRetry(async () => {
try {
await client.delete(`${baseUrl}/api/v4/custom_profile_attributes/fields/${fieldId}`);
return {};
} catch (err) {
return getResponseFromError(err);
}
}, {idempotent: true, label: 'apiDeleteCustomProfileAttributeField', budgetMs: timeouts.ONE_MIN});
};
/**
* Find an existing custom profile attribute field by name, or create it if absent.
* Returns {field, created} where created=true means this call created the field
* and the caller is responsible for deleting it in cleanup.
* @param {string} baseUrl - the base server URL
* @param {string} name - field name to look up or create
* @param {string} [type] - field type when creating, defaults to 'text'
* @return {Object} returns {field, created} on success or {error, status} on error
*/
export const apiGetOrCreateCustomProfileAttributeField = async (
baseUrl: string,
name: string,
type = 'text',
): Promise<any> => {
const {fields, error: listError} = await apiGetCustomProfileAttributeFields(baseUrl);
if (listError) {
return {error: listError};
}
const existing = fields.find((f: any) => f.name === name);
if (existing) {
return {field: existing, created: false};
}
const {field, error: createError} = await apiCreateCustomProfileAttributeField(baseUrl, name, type);
if (createError) {
return {error: createError};
}
return {field, created: true};
};
/**
* Create a custom profile attribute field.
* The returned field ID is required when setting attribute values on users.
* @param {string} baseUrl - the base server URL
* @param {string} name - display name for the field, e.g. "Department"
* @param {string} [type] - field type, defaults to 'text'
* @return {Object} returns {field} on success or {error, status} on error
*/
export const apiCreateCustomProfileAttributeField = async (
baseUrl: string,
name: string,
type = 'text',
): Promise<any> => {
try {
const response = await client.post(
`${baseUrl}/api/v4/custom_profile_attributes/fields`,
{
name,
type,
attrs: {
visibility: 'when_set',
sort_order: 0,
},
},
);
return {field: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Set one or more property values on a user.
* Used to assign ABAC-relevant attributes (e.g. Department) before policy evaluation.
* @param {string} baseUrl - the base server URL
* @param {string} userId - the target user ID
* @param {Record<string, string>} values - map of field ID → value, e.g. { [fieldId]: 'Engineering' }
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiSetUserPropertyValues = async (
baseUrl: string,
userId: string,
values: Record<string, string>,
): Promise<any> => {
return withTransportRetry(async () => {
try {
const response = await client.patch(
`${baseUrl}/api/v4/users/${userId}/custom_profile_attributes`,
values,
);
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
}, {idempotent: true, label: 'apiSetUserPropertyValues', budgetMs: timeouts.ONE_MIN});
};
// ------------------------------------------------------------
// Permission Policies
// Controls which users can perform actions (e.g. download_file_attachment).
// CEL expression options:
// "false" → denies everyone unconditionally
// "true" → grants everyone (no restriction)
// "user.attributes.Department == \"Engineering\"" → grants only matching users
// ------------------------------------------------------------
/**
* Create an ABAC permission policy.
* Requires admin session and ABAC to be enabled.
* @param {string} baseUrl - the base server URL
* @param {string} name - unique policy name
* @param {Array<{actions: string[]; expression: string}>} rules - policy rules; each rule maps actions to a CEL expression
* @param {string[]} [roles] - roles the policy applies to, e.g. ['system_user']. Defaults to ['system_user']
* @return {Object} returns {policy} on success or {error, status} on error
*/
export const apiCreatePermissionPolicy = async (
baseUrl: string,
name: string,
rules: Array<{actions: string[]; expression: string}>,
roles: string[] = ['system_user'],
): Promise<any> => {
try {
const response = await client.put(
`${baseUrl}/api/v4/access_control_policies`,
{
name,
type: 'permission',
roles,
rules,
},
);
return {policy: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Create a channel access policy whose rules gate permission actions in that channel only; channel
* membership is unaffected. The policy id is the channel id, so delete it with apiDeletePermissionPolicy.
* Requires admin session and the ChannelPermissionPolicies feature flag.
* @param {string} baseUrl - the base server URL
* @param {string} channelId - the channel the policy applies to
* @param {Array<{name: string; role: string; actions: string[]; expression: string}>} rules - v0.4 rules; each needs a unique name and a channel role such as 'channel_user'
* @return {Object} returns {policy} on success or {error, status} on error
*/
export const apiCreateChannelPermissionPolicy = async (
baseUrl: string,
channelId: string,
rules: Array<{name: string; role: string; actions: string[]; expression: string}>,
): Promise<any> => {
return withTransportRetry(async () => {
try {
const response = await client.put(
`${baseUrl}/api/v4/access_control_policies`,
{id: channelId, type: 'channel', version: 'v0.4', rules},
);
return {policy: response.data};
} catch (err) {
return getResponseFromError(err);
}
}, {idempotent: true, label: 'apiCreateChannelPermissionPolicy', budgetMs: timeouts.ONE_MIN});
};
/**
* Delete an access control policy by ID.
* Requires admin session.
* @param {string} baseUrl - the base server URL
* @param {string} policyId - the policy ID to delete
* @return {Object} returns {} on success or {error, status} on error
*/
export const apiDeletePermissionPolicy = async (baseUrl: string, policyId: string): Promise<any> => {
return withTransportRetry(async () => {
try {
await client.delete(`${baseUrl}/api/v4/access_control_policies/${policyId}`);
return {};
} catch (err) {
return getResponseFromError(err);
}
}, {idempotent: true, label: 'apiDeletePermissionPolicy', budgetMs: timeouts.ONE_MIN});
};
// ------------------------------------------------------------
// Channel (Membership) Policies
// Controls which users can access / remain members of a channel
// based on their attributes (CEL expression evaluated per user).
// ------------------------------------------------------------
/**
* Activate or deactivate an access control policy.
* A policy must be active to be enforced by the server.
* Uses PUT /api/v4/access_control_policies/activate with entries array.
* @param {string} baseUrl - the base server URL
* @param {string} policyId - the policy ID
* @param {boolean} [active] - true to activate (default), false to deactivate
* @return {Object} returns {} on success or {error, status} on error
*/
export const apiSetPolicyActive = async (
baseUrl: string,
policyId: string,
active = true,
): Promise<any> => {
return withTransportRetry(async () => {
try {
await client.put(
`${baseUrl}/api/v4/access_control_policies/activate`,
{entries: [{id: policyId, active}]},
);
return {};
} catch (err) {
return getResponseFromError(err);
}
}, {idempotent: true, label: 'apiSetPolicyActive', budgetMs: timeouts.ONE_MIN});
};
export const AccessControl = {
apiEnableABAC,
apiGetCustomProfileAttributeFields,
apiGetOrCreateCustomProfileAttributeField,
apiCreateCustomProfileAttributeField,
apiDeleteCustomProfileAttributeField,
apiSetUserPropertyValues,
apiCreatePermissionPolicy,
apiCreateChannelPermissionPolicy,
apiDeletePermissionPolicy,
apiSetPolicyActive,
};
export default AccessControl;