diff --git a/docs/main/administration-guide/configure/environment-configuration-settings.mdx b/docs/main/administration-guide/configure/environment-configuration-settings.mdx index 156dddbd6628..34850ac004de 100644 --- a/docs/main/administration-guide/configure/environment-configuration-settings.mdx +++ b/docs/main/administration-guide/configure/environment-configuration-settings.mdx @@ -2587,7 +2587,7 @@ See the [Telemetry](/administration-guide/manage/telemetry#security-update-check With self-hosted deployments, you can configure Mattermost as a [high availability cluster-based deployment](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) by going to **System Console \> Environment \> High Availability**, or by editing the `config.json` file as described in the following tables. Changes to configuration settings in this section require a server restart before taking effect. -In a Mattermost high availability cluster-based deployment, the System Console is set to read-only, and settings can only be changed by editing the `config.json` file directly. However, to test a high availability cluster-based environment, you can disable `ClusterSettings.ReadOnlyConfig` in the `config.json` file by setting it to `false`. This allows changes applied using the System Console to be saved back to the configuration file. +When a Mattermost high availability cluster-based deployment uses `config.json` for configuration, the System Console is set to read-only by default, and settings can only be changed by editing the `config.json` file directly. However, to test a high availability cluster-based environment, you can disable `ClusterSettings.ReadOnlyConfig` in the `config.json` file by setting it to `false`. This allows changes applied using the System Console to be saved back to the configuration file. When configuration is stored in the database, `ClusterSettings.ReadOnlyConfig` is ignored and the System Console remains fully editable. See [Store configuration in your database](/administration-guide/configure/configuration-in-your-database) for details. ### Enable high availability mode diff --git a/docs/main/administration-guide/manage/admin/migration.mdx b/docs/main/administration-guide/manage/admin/migration.mdx index 5c41d1cb8821..4e67f8a518ac 100644 --- a/docs/main/administration-guide/manage/admin/migration.mdx +++ b/docs/main/administration-guide/manage/admin/migration.mdx @@ -6,6 +6,7 @@ This Mattermost Migration Guide is organized into sections based on migration sc Whether you’re migrating from another platform, upgrading your database, or using bulk tools for data management, this guide provides the resources and instructions you need for a successful migration. Use the navigation below to explore detailed guidance tailored to your migration needs. - [Migrate from MySQL to PostgreSQL](/deployment-guide/postgres-migration) - Learn how to migrate from MySQL to PostgreSQL. +- [Migrate to FIPS-compliant images](/deployment-guide/server/fips-migration) - Learn how to switch an existing Docker or Kubernetes deployment to the FIPS-compliant Mattermost images. - [Server migration guide](/administration-guide/onboard/migrating-to-mattermost) - Learn about about migrating to Mattermost. - [Migrate from Slack](/administration-guide/onboard/migrate-from-slack) - Learn how to migrate from Slack to Mattermost. - [Migrate from Rocket.Chat](/administration-guide/onboard/migrate-from-rocketchat) - Learn how to migrate from Rocket.Chat to Mattermost. diff --git a/docs/main/administration-guide/onboard/sso-saml-keycloak.mdx b/docs/main/administration-guide/onboard/sso-saml-keycloak.mdx index b154f6ab434d..2558da4f48c3 100644 --- a/docs/main/administration-guide/onboard/sso-saml-keycloak.mdx +++ b/docs/main/administration-guide/onboard/sso-saml-keycloak.mdx @@ -45,7 +45,7 @@ This was tested with Keycloak v26.4.0. We recommend adding Mattermost as a clien > - **Sign Documents**: **Off** > - **Sign Assertions**: **On** -> - **Signature Algorithm**: `RSA_SHA1` +> - **Signature Algorithm**: `RSA_SHA256` > - **SAML signature key name**: `NONE` > - **Canonicalization Method**: `EXCLUSIVE` > - **Encryption Algorithm**: `AES_256_GCM` @@ -54,15 +54,15 @@ This was tested with Keycloak v26.4.0. We recommend adding Mattermost as a clien > > ![In Keycloak, configure the Signature and Encryption settings for the Mattermost client.](/images/keycloak_1_client_signature_encryption.png) > ->
+>
> >
> -> Warning +> Note > >
> -> Mattermost only supports `RSA_SHA1` for Keycloak SAML, because Keycloak is using `xmlenc` for `RSA_SHA512` and `RSA_SHA256`, wheras Mattermost currently only supports `xmldsig`. So make sure to use `RSA_SHA1` as the *Signature Algorithm* and *Digest Method* as described in this section. +> Earlier versions of this documentation recommended `RSA_SHA1`. Use `RSA_SHA256` instead. [NIST SP 800-131A](https://csrc.nist.gov/pubs/sp/800/131/a/r2/final) set December 31, 2013 as the deadline after which SHA-1 is disallowed for digital signature generation. Revision 2 of that publication, published in March 2019, allows SHA-1 signature generation only where NIST protocol-specific guidance specifically permits it. A Keycloak server running in FIPS mode returns a `500` error when it tries to sign the assertion with `RSA_SHA1`. Mattermost validates `RSA_SHA256`-signed assertions, and Keycloak derives the signature digest method from the signature algorithm you select here. > >
@@ -86,7 +86,37 @@ This was tested with Keycloak v26.4.0. We recommend adding Mattermost as a clien > >
> -> Mattermost does not support request signing with Keycloak so make sure to disable the Client signature setting as mentioned above. +> Leave **Client signature required** off, as shown above. Mattermost does sign its authentication requests when you enable **Sign Request** in step 4 of the Mattermost section below, but Keycloak validates those signatures only if you also import the Mattermost service provider certificate as a signing key on this tab. Turning **Client signature required** on without importing that certificate causes Keycloak to reject Mattermost's authentication requests. +> +> +> +>
+> +>
+> +> Note +> +>
+> +> **If your Keycloak server runs in FIPS mode**, the **Archive Format** list offers only `BCFKS`, because `PKCS12` and `JKS` are unavailable. Export the `BCFKS` keystore, then convert it to `PKCS12` before you continue: +> +> ```console +> keytool -importkeystore -srckeystore keystore.bcfks -srcstoretype BCFKS -providerclass org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider -providerpath /path/to/bc-fips-.jar -destkeystore keystore.p12 -deststoretype PKCS12 +> ``` +> +> Point `-providerpath` at your Bouncy Castle FIPS JAR. `keytool` prompts for the source and destination store passwords. The converted `keystore.p12` then works with the extraction commands in step 4 of Configure SAML in Mattermost below. +> +> Convert the whole keystore as shown above rather than looking for a way to export the key on its own, because `keytool` has no command that exports a private key by itself. +> +> As an alternative, generating the keypair locally and importing only the certificate into Keycloak avoids the keystore conversion entirely, and works on FIPS and non-FIPS servers alike: +> +> ```console +> openssl req -x509 -newkey rsa:2048 -nodes -days 1095 -subj "/CN=mattermost" -keyout mattermost.key -out mattermost.crt +> ``` +> +> Keep `-nodes`, because Mattermost can't read a passphrase-protected private key. Set `-days` explicitly, because it defaults to 30. Keep `-keyout` and `-out`, because without them the certificate goes to standard output and the private key goes to a configuration-dependent default filename. +> +> Import only `mattermost.crt` into the encryption keys section of this tab, then upload both files in step 4 below and skip the `openssl pkcs12` extraction commands, since you already have the files those commands would have produced. > >
> diff --git a/docs/main/administration-guide/upgrade/important-upgrade-notes.mdx b/docs/main/administration-guide/upgrade/important-upgrade-notes.mdx index e49c17f4beb0..1c44f2ba2432 100644 --- a/docs/main/administration-guide/upgrade/important-upgrade-notes.mdx +++ b/docs/main/administration-guide/upgrade/important-upgrade-notes.mdx @@ -21,7 +21,10 @@ We recommend reviewing the [additional upgrade notes](#additional-upgrade-notes) -v11.10 +v11.10 +

Starting with v11.10.1, the updated glibc-openssl-fips bundle includes a new OpenSSL build that enforces the FIPS minimum key length of 112 bits for HMAC operations. The PostgreSQL driver (lib/pq) passes the database password as the HMAC key during SCRAM authentication, so a password shorter than 112 bits, or 14 ASCII characters, now causes a panic on connect rather than a graceful failure. Before upgrading, FIPS deployments should verify that the password in SqlSettings.DataSource is at least 14 characters and rotate it in PostgreSQL if it is shorter. Standard, non-FIPS builds are unaffected.

+ +

This migration adds a new composite index, idx_propertyvalues_groupid_updateat_id, on the PropertyValues table covering the columns GroupID, UpdateAt, and ID. The PropertyValues table is part of the Properties/Custom Attributes feature introduced in recent Mattermost versions. This index improves query performance for lookups and range scans that filter or sort by GroupID and UpdateAt, which are common access patterns for this feature. The index is created using CONCURRENTLY, so the build process does not block concurrent reads or writes against the PropertyValues table. The migrations are fully backwards-compatible and no database downtime is expected for this upgrade. The SQL queries included are:

{"-- morph:nontransactional\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_propertyvalues_groupid_updateat_id\n    ON PropertyValues(GroupID, UpdateAt, ID);"}
{"-- morph:nontransactional\nDROP INDEX CONCURRENTLY IF EXISTS idx_propertyvalues_groupid_updateat_id;"}
@@ -393,7 +396,10 @@ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_channeljoinrequests_user_status_crea ALTER TYPE permission_level ADD VALUE IF NOT EXISTS 'admin'; -v11.7 +v11.7 +Starting with v11.7.10, the updated glibc-openssl-fips bundle includes a new OpenSSL build that enforces the FIPS minimum key length of 112 bits for HMAC operations. The PostgreSQL driver (lib/pq) passes the database password as the HMAC key during SCRAM authentication, so a password shorter than 112 bits, or 14 ASCII characters, now causes a panic on connect rather than a graceful failure. Before upgrading, FIPS deployments should verify that the password in SqlSettings.DataSource is at least 14 characters and rotate it in PostgreSQL if it is shorter. Standard, non-FIPS builds are unaffected. + + FIPS builds require a minimum of 14 characters for passwords, atmos/camo proxy configuration, and shared channel secrets. Shorter passwords for existing users will no longer be valid and require a password reset. Non-FIPS builds are unaffected. diff --git a/docs/main/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment.mdx b/docs/main/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment.mdx index 899f073f4c39..4a0e51309e74 100644 --- a/docs/main/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment.mdx +++ b/docs/main/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment.mdx @@ -208,10 +208,17 @@ Back up your Mattermost database and file storage locations before configuring h mmctl config set ClusterSettings.Enable true mmctl config set ClusterSettings.ClusterName production mmctl config set ClusterSettings.UseIPAddress true - mmctl config set ClusterSettings.ReadOnlyConfig true mmctl config set ClusterSettings.GossipPort 8074 ``` + If you're storing configuration in `config.json` rather than the database, also set `ClusterSettings.ReadOnlyConfig` to `true` to prevent System Console changes from writing to one node's `config.json` and diverging from other nodes: + + ```bash + mmctl config set ClusterSettings.ReadOnlyConfig true + ``` + + When configuration is stored in the database (see [Store configuration in your database](/administration-guide/configure/configuration-in-your-database)), `ClusterSettings.ReadOnlyConfig` is ignored because all nodes read the same configuration from the shared database. + See the [high availability configuration settings](/administration-guide/configure/environment-configuration-settings#high-availability) documentation for details on all available cluster settings, including `OverrideHostname` for non-standard network configurations. 8. **Restart Mattermost servers:** Restart each Mattermost server in the cluster to apply the new configuration. diff --git a/docs/main/deployment-guide/server/containers/fips-stig.mdx b/docs/main/deployment-guide/server/containers/fips-stig.mdx index b6c73f15bf2b..bd0473655e4f 100644 --- a/docs/main/deployment-guide/server/containers/fips-stig.mdx +++ b/docs/main/deployment-guide/server/containers/fips-stig.mdx @@ -22,6 +22,6 @@ All application-level code uses only FIPS-approved algorithms, ensuring that cry - The Mattermost FIPS image includes only prepackaged Boards, Playbooks, and Agents. Additional plugins can be added to the Mattermost FIPS image, but they will run in non-FIPS mode. -- Existing Docker or Kubernetes-based deployments can change the image from `mattermost/mattermost-enterprise-edition` to `mattermost/mattermost-enterprise-fips-edition`. +- Existing Docker or Kubernetes-based deployments can change the image from `mattermost/mattermost-enterprise-edition` to `mattermost/mattermost-enterprise-fips-edition`. See [Migrate an existing deployment to FIPS-compliant images](/deployment-guide/server/fips-migration) for step-by-step instructions. diff --git a/docs/main/deployment-guide/server/fips-migration.mdx b/docs/main/deployment-guide/server/fips-migration.mdx new file mode 100644 index 000000000000..d61e1ad0cb6b --- /dev/null +++ b/docs/main/deployment-guide/server/fips-migration.mdx @@ -0,0 +1,145 @@ +--- +title: "Migrate an existing deployment to FIPS-compliant images" +sidebar_label: "Migrate to FIPS-compliant images" +--- + + +From Mattermost v11, each release ships in two image variants: a standard Enterprise build (`mattermost/mattermost-enterprise-edition`) and a FIPS-compliant build (`mattermost/mattermost-enterprise-fips-edition`). Migrating an existing deployment to the FIPS image is primarily a matter of replacing the image and restarting Mattermost. No data migration is required, and rollback is symmetric. + +This guide covers migrating an existing Mattermost Server deployment running on Docker or Kubernetes. Linux package and tarball installations have no FIPS-compliant equivalent. + +Mattermost's FIPS offering also covers the Mattermost Operator controller and a self-hosted Push Proxy. Migrating those components to their FIPS variants is out of scope for this guide. + +For background on what the FIPS build is and how it's constructed, see the **FIPS/STIG** tab on [Deploy Mattermost using Containers](/deployment-guide/server/deploy-containers). + +## Before you begin + +1. **Back up your database and configuration.** Always take a full backup before changing the image. See the [backup and disaster recovery](/deployment-guide/backup-disaster-recovery) documentation. + +2. **Confirm your deployment type.** FIPS images are supported on Docker, Docker Compose, and Kubernetes only. If you're running Mattermost from a Linux package or tarball, you can't migrate in place. + +3. **Check your Mattermost version.** FIPS images are available from v11.0 onward and use the same release tags as the standard Enterprise images. Plan to migrate to the matching FIPS tag for your current version. + +4. **Plan for additional plugins.** The FIPS image includes Boards, Playbooks, and Agents prepackaged and running in FIPS mode. Any additional plugins you've installed will continue to run inside the FIPS image, but they run in non-FIPS mode. This is expected behavior, not a configuration error. + +5. **PostgreSQL password length.** Beginning with Mattermost v11.7, the Postgres password used by the Mattermost server must be at least 14 characters when running the FIPS image. If your current password is shorter, complete these steps before swapping the image: + + a. Rotate the password in PostgreSQL. For example, connecting as a Postgres superuser and replacing `mmuser` and `` to match your deployment: + + ```sql + ALTER USER mmuser WITH PASSWORD ''; + ``` + + b. Update the password where your deployment stores it: in the official Docker deployment, the `POSTGRES_PASSWORD` value in `.env`; in Operator-managed Kubernetes deployments, the database Secret referenced by your `Mattermost` custom resource (commonly under `spec.database.external.secret`). + + c. Proceed to the image swap procedure for your deployment type below. Recreating the container as part of the migration applies the new credentials — no separate restart is required. + +6. **Plan for downtime.** The migration requires pulling the new image and restarting the Mattermost container or pod. + +## Migrate a Kubernetes deployment + +These steps assume your deployment is managed by the Mattermost Operator using a `Mattermost` custom resource. + +In the steps below, replace `` with the namespace your Mattermost installation runs in. If you omit `-n `, `kubectl` uses your current context's default namespace. + +1. Find the name of your Mattermost custom resource: + + ```sh + kubectl -n get mattermost + ``` + +2. Edit the `Mattermost` custom resource to point at the FIPS image. You can edit the live resource directly: + + ```sh + kubectl -n edit mattermost + ``` + + Or update your manifest file and re-apply it. + +3. In `spec.image`, change the value from `mattermost/mattermost-enterprise-edition` to `mattermost/mattermost-enterprise-fips-edition`. Keep `spec.version` aligned with your current release tag. For example: + + ```yaml + spec: + image: mattermost/mattermost-enterprise-fips-edition + version: 11.6.1 + ``` + +4. If you edited a manifest file, apply it: + + ```sh + kubectl -n apply -f .yaml + ``` + +5. Watch the Mattermost pods until the new ones report `Ready` and the old ones terminate. A pod showing `Running` isn't necessarily serving traffic yet — wait until its `READY` column shows all containers ready (for example, `1/1`): + + ```sh + kubectl -n get pods -l app=mattermost -w + ``` + +6. Verify the running pods are using the FIPS image: + + ```sh + kubectl -n get pods -l app=mattermost -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[*].image}{"\n"}{end}' + ``` + +## Migrate a Docker or Docker Compose deployment + +These steps are written for the official [Mattermost Docker deployment](https://github.com/mattermost/docker), which selects the Mattermost image and tag through `.env` variables (`MATTERMOST_IMAGE` and `MATTERMOST_IMAGE_TAG`) referenced from `docker-compose.yml` as `mattermost/${MATTERMOST_IMAGE}:${MATTERMOST_IMAGE_TAG}`. If you're using a custom Docker setup that hardcodes the image in `docker-compose.yml` or uses different variable names, adapt these steps accordingly. + +1. Stop the Mattermost container: + + ```sh + docker compose stop mattermost + ``` + +2. Edit your `.env` file. Change `MATTERMOST_IMAGE` from `mattermost-enterprise-edition` to `mattermost-enterprise-fips-edition`. Leave `MATTERMOST_IMAGE_TAG` set to your current release tag — FIPS images are published under the same tags as the standard Enterprise images. + + ```text + MATTERMOST_IMAGE=mattermost-enterprise-fips-edition + MATTERMOST_IMAGE_TAG= + ``` + +3. Pull the FIPS image: + + ```sh + docker compose pull mattermost + ``` + +4. Recreate the Mattermost container so the new `.env` values are applied: + + ```sh + docker compose up -d --force-recreate mattermost + ``` + +5. Verify the running container is using the FIPS image: + + ```sh + docker inspect mattermost --format '{{.Config.Image}}' + ``` + +## Verify the migration + +1. Confirm Mattermost starts cleanly. Tail the logs and watch for startup errors. On Kubernetes, target a specific Mattermost pod: + + ```sh + kubectl -n logs -f + ``` + + On Docker: + + ```sh + docker compose logs -f mattermost + ``` + +2. Sign in and verify core functionality (sending messages, file uploads, search). + +3. Confirm the prepackaged plugins (Boards, Playbooks, Agents) load successfully. In the System Console, go to **Plugins > Plugin Management** and confirm they're enabled and healthy. + +4. If you have additional plugins installed, confirm they still load. They'll run in non-FIPS mode inside the FIPS image — this is expected. + +## Roll back + +If the migration doesn't go as planned, rolling back is symmetric: revert the image reference and redeploy. No data migration is involved. + +- **Kubernetes:** Edit the `Mattermost` custom resource and change `spec.image` back to `mattermost/mattermost-enterprise-edition`. Re-apply or save, and watch the rollout. +- **Docker / Docker Compose:** Restore the original `MATTERMOST_IMAGE` value in `.env` (`mattermost-enterprise-edition`) and run `docker compose pull mattermost` followed by `docker compose up -d --force-recreate mattermost`. diff --git a/docs/main/deployment-guide/server/linux/deploy-rhel.mdx b/docs/main/deployment-guide/server/linux/deploy-rhel.mdx index 327a91ad663d..7a86f615f15f 100644 --- a/docs/main/deployment-guide/server/linux/deploy-rhel.mdx +++ b/docs/main/deployment-guide/server/linux/deploy-rhel.mdx @@ -37,14 +37,14 @@ SSH onto the target host and download the release. Replace `amd64` with `arm64` ```sh -wget https://releases.mattermost.com/11.10.0/mattermost-11.10.0-linux-amd64.tar.gz +wget https://releases.mattermost.com/11.10.1/mattermost-11.10.1-linux-amd64.tar.gz ``` ```sh -wget https://releases.mattermost.com/11.7.9/mattermost-11.7.9-linux-amd64.tar.gz +wget https://releases.mattermost.com/11.7.10/mattermost-11.7.10-linux-amd64.tar.gz ``` diff --git a/docs/main/deployment-guide/server/linux/deploy-tar.mdx b/docs/main/deployment-guide/server/linux/deploy-tar.mdx index f4f174af26e3..b322106ccc8a 100644 --- a/docs/main/deployment-guide/server/linux/deploy-tar.mdx +++ b/docs/main/deployment-guide/server/linux/deploy-tar.mdx @@ -48,14 +48,14 @@ SSH onto the target host and download the release. Replace `amd64` with `arm64` ```sh -wget https://releases.mattermost.com/11.10.0/mattermost-11.10.0-linux-amd64.tar.gz +wget https://releases.mattermost.com/11.10.1/mattermost-11.10.1-linux-amd64.tar.gz ``` ```sh -wget https://releases.mattermost.com/11.7.9/mattermost-11.7.9-linux-amd64.tar.gz +wget https://releases.mattermost.com/11.7.10/mattermost-11.7.10-linux-amd64.tar.gz ``` diff --git a/docs/main/deployment-guide/server/server-deployment-planning.mdx b/docs/main/deployment-guide/server/server-deployment-planning.mdx index 42c013c48638..6db4e9ffb094 100644 --- a/docs/main/deployment-guide/server/server-deployment-planning.mdx +++ b/docs/main/deployment-guide/server/server-deployment-planning.mdx @@ -7,6 +7,7 @@ This section provides comprehensive guidance on deploying and managing your Matt - [Deploy with Kubernetes](/deployment-guide/server/deploy-kubernetes) - Scalable deployment on various Kubernetes platforms with high availability support. - [Deploy with Linux](/deployment-guide/server/deploy-linux) - Direct installation on Linux servers for full control over the deployment. - [Deploy with Containers](/deployment-guide/server/deploy-containers) - Docker-based deployment suitable for smaller installations. +- [Migrate to FIPS-compliant images](/deployment-guide/server/fips-migration) - Switch an existing Docker or Kubernetes deployment to the FIPS-compliant Mattermost images. - [Pre-authentication secrets](/deployment-guide/server/pre-authentication-secrets) - Configure reverse proxy validation for mobile and desktop applications using pre-authentication headers. - [Deployment Solution Programs](/deployment-guide/server/orchestration) - Automated deployment tools and orchestration solutions. diff --git a/docs/main/product-overview/deprecated-features.mdx b/docs/main/product-overview/deprecated-features.mdx index e5092d5ae079..06e976113a98 100644 --- a/docs/main/product-overview/deprecated-features.mdx +++ b/docs/main/product-overview/deprecated-features.mdx @@ -12,10 +12,15 @@ This page describes features that are removed from support for Mattermost, or wi - Starting with Mattermost Server v12.0 (October 2026), ``atmos/camo`` will no longer be supported as an image proxy. The ``atmos/camo`` project has been archived and is no longer maintained. If ``ImageProxySettings.ImageProxyType`` is set to ``atmos/camo``, the server will log a configuration error on startup and fail to start; the ``RemoteImageProxyURL`` and ``RemoteImageProxyOptions`` settings are also removed and will be ignored. Admins should switch to the built-in local image proxy (set ``ImageProxySettings.ImageProxyType`` to ``local``) or disable image proxying entirely (``ImageProxySettings.Enable`` set to ``false``) before upgrading. The local proxy requires no external service and includes SVG content blocking and security response headers. See the [forum post](https://forum.mattermost.com/t/starting-with-mattermost-v12-0-october-2026-atmos-camo-is-no-longer-supported-as-an-image-proxy/25980) for full details and migration options. - Starting with v12.0 (October release), the Mattermost web and desktop apps will be built on [React 19](https://react.dev/blog/2024/04/25/react-19-upgrade-guide). This only affects plugins that register web app (frontend) components — server-only plugins are unaffected. Web app plugins use the React version provided by the web app, so your plugin's components will render under React 19 whether or not you rebuild. Plugins that call APIs removed in React 19 (``ReactDOM.findDOMNode``, ``propTypes``/``defaultProps`` on function components, legacy Context, string refs) or depend on legacy synchronous rendering may break. To prepare: run ``npx codemod@latest run react-19-migration-recipe``, fix any removed-API usage, and test your plugin against a v12.0 release candidate during the beta window. See the [forum post](https://forum.mattermost.com/t/starting-with-mattermost-v12-0-october-2026-the-mattermost-web-and-desktop-apps-are-built-on-react-19/26038) for full details. -- Starting with v12.0 (October release), the Mattermost mobile app will require your server to be running v10.11 or later. Server v10.11 reaches End of Support on **August 15, 2026** as scheduled. Separately, in **October 2026** the mobile app will begin requiring server **v10.11 or later** — users on earlier servers won't be able to sign in from mobile. The minimum tracks the most recently retired [ESR](https://docs.mattermost.com/product-overview/release-policy.html#extended-support-releases), so the next bump is to **v11.7** in 2027. Deployments on MySQL will need to migrate to PostgreSQL to move past v10.11. See the [forum post](https://forum.mattermost.com/t/starting-in-october-2026-the-mattermost-mobile-app-will-require-mattermost-server-v10-11-or-later/26039) for full details. - Starting with Mattermost Server v12.0 (October 2026), user sessions and personal access tokens (PATs) can no longer set post identity or display-override props. Some deployments have relied on forging props such as ``from_webhook`` with ``override_username`` and ``override_icon_url`` so posts appear under a custom name and icon — a pattern indistinguishable from impersonation. In v12.0, the server strips these props from client and PAT payloads and re-applies them only under verified integration authority. Posts are still created, but forged props are silently removed and the message appears as the authenticating user, with no error returned. Legitimate integrations are unaffected: incoming webhooks and slash commands can still override the username and icon when enabled in the System Console, and bots post as the bot account. Admins relying on PAT forging should migrate those scripts to an incoming webhook or bot account before upgrading. See the [forum post](https://forum.mattermost.com/t/starting-with-mattermost-v12-0-october-2026-user-and-personal-access-token-pat-sessions-can-no-longer-set-post-identity-or-display-override-props/26052) for full details and migration options. - Starting with Mattermost Server v12.0 (October 2026), deprecated interactive dialog ``date/datetime`` fields will be removed. Top-level ``min_date``, ``max_date``, and ``time_interval``, and ``datetime_config.allow_manual_time_entry``, will no longer be accepted; use ``datetime_config`` (with ``manual_time_entry``) instead. Legacy keys will be silently ignored. - Starting with Mattermost Server v12.0 (October 2026), deprecated Slack compatibility type aliases and functions will be removed (``SlackAttachment``, ``SlackAttachmentField``, ``ParseSlackAttachment``, ``StringifySlackFieldValue``) from the go package. Use ``MessageAttachment``, ``MessageAttachmentField``, ``ParseMessageAttachment``, and ``StringifyMessageAttachmentFieldValue`` instead. +- Starting with Mattermost Server v12.0 (October 2026), a set of unused configuration settings will be removed: ``ElasticsearchSettings.BulkIndexingTimeWindowSeconds``, ``ClusterSettings.EnableExperimentalGossipEncryption``, ``ExperimentalSettings.ClientSideCertEnable``, ``TeamSettings.ExperimentalViewArchivedChannels``, ``CloudSettings.CWSMock``, ``PluginSettings.ChimeraOAuthProxyURL``, ``DataRetentionSettings.EnableBoardsDeletion``, ``DataRetentionSettings.BoardsRetentionDays``, ``SqlSettings.AtRestEncryptKey``, ``NativeAppSettings.EnableIntuneMAM``, and ``GuestAccountsSettings.AllowEmailAccounts``. Removed fields present in an existing ``config.json`` are ignored, and the server will start normally, so no configuration changes are required before upgrading. Admins using ``NativeAppSettings.EnableIntuneMAM`` should use ``IntuneSettings.Enable`` instead. + +### Mattermost Mobile App v2.45 (October 2026) + +- Starting with Mattermost Mobile App v2.45 (October 2026), the Mattermost mobile app will require your server to be running v10.11 or later. Server v10.11 reached End of Support on **August 15, 2026**. The minimum tracks the most recently retired [ESR](https://docs.mattermost.com/product-overview/release-policy.html#extended-support-releases), so the next bump is to **v11.7** in 2027. Deployments on MySQL will need to migrate to PostgreSQL to move past v10.11. See the [forum post](https://forum.mattermost.com/t/starting-in-october-2026-the-mattermost-mobile-app-will-require-mattermost-server-v10-11-or-later/26039) for full details. +- Starting with Mattermost Mobile App v2.45 (October 2026), the minimum supported iOS version will be v17. Devices running iOS v16 or earlier will no longer receive new versions of the app from the App Store; the installed version will continue to work but will not receive further updates, including security fixes. Users should update to iOS v17 or newer before the v2.45 release. See the [forum post](https://forum.mattermost.com/t/deprecation-notice-ios-16-versions/26074) for full details. ### Mattermost Desktop App v6.4 (November 2026) diff --git a/docs/main/product-overview/mattermost-mobile-releases.mdx b/docs/main/product-overview/mattermost-mobile-releases.mdx index 75de2cea792c..d054839bf7b0 100644 --- a/docs/main/product-overview/mattermost-mobile-releases.mdx +++ b/docs/main/product-overview/mattermost-mobile-releases.mdx @@ -24,7 +24,7 @@ We strongly recommend using the latest mobile app release available that contain | **Release** | **Support** | **Compatible with** | |:---|:---|:---| -| v2.43 [FEATURE](./mobile-app-changelog#release-v2-43-0) \| [Download](https://github.com/mattermost/mattermost-mobile/releases/tag/v2.43.0) \| [Changelog](./mobile-app-changelog#release-v2-43-0) \| [SBOM download](https://github.com/mattermost/mattermost-mobile/releases/download/v2.43.0/sbom-mattermost-mobile-v2.43.0.json) | Released: 2026-08-14
Support Ends: 2026-09-15 | [v11.10](./mattermost-v11-changelog#release-v11-10-feature-release), [v11.9](./mattermost-v11-changelog#release-v11-9-feature-release), [v11.8](./mattermost-v11-changelog#release-v11-8-feature-release), [v11.7](./mattermost-v11-changelog#release-v11-7-extended-support-release) | +| v2.43 [FEATURE](./mobile-app-changelog#release-v2-43-1) \| [Download](https://github.com/mattermost/mattermost-mobile/releases/tag/v2.43.1) \| [Changelog](./mobile-app-changelog#release-v2-43-1) \| [SBOM download](https://github.com/mattermost/mattermost-mobile/releases/download/v2.43.1/sbom-mattermost-mobile-v2.43.1.json) | Released: 2026-08-14
Support Ends: 2026-09-15 | [v11.10](./mattermost-v11-changelog#release-v11-10-feature-release), [v11.9](./mattermost-v11-changelog#release-v11-9-feature-release), [v11.8](./mattermost-v11-changelog#release-v11-8-feature-release), [v11.7](./mattermost-v11-changelog#release-v11-7-extended-support-release) | | v2.42 [FEATURE](./mobile-app-changelog#release-v2-42-3) \| [Download](https://github.com/mattermost/mattermost-mobile/releases/tag/v2.42.3) \| [Changelog](./mobile-app-changelog#release-v2-42-3) \| [SBOM download](https://github.com/mattermost/mattermost-mobile/releases/download/v2.42.3/sbom-mattermost-mobile-v2.42.3.json) | Released: 2026-07-16
Support Ends: 2026-08-15 | [v11.9](./mattermost-v11-changelog#release-v11-9-feature-release), [v11.8](./mattermost-v11-changelog#release-v11-8-feature-release), [v11.7](./mattermost-v11-changelog#release-v11-7-extended-support-release), [v10.11](./mattermost-v10-changelog#release-v10-11-extended-support-release) | | v2.41 [FEATURE](./mobile-app-changelog#release-v2-41-3) \| [Download](https://github.com/mattermost/mattermost-mobile/releases/tag/v2.41.3) \| [Changelog](./mobile-app-changelog#release-v2-41-3) \| [SBOM download](https://github.com/mattermost/mattermost-mobile/releases/download/v2.41.3/sbom-mattermost-mobile-v2.41.3.json) | Released: 2026-06-16
Support Ends: 2026-07-15 | [v11.8](./mattermost-v11-changelog#release-v11-8-feature-release), [v11.7](./mattermost-v11-changelog#release-v11-7-extended-support-release), [v11.6](./mattermost-v11-changelog#release-v11-6-feature-release), [v10.11](./mattermost-v10-changelog#release-v10-11-extended-support-release) | | v2.40 [FEATURE](./mobile-app-changelog#release-v2-40-0) \| [Download](https://github.com/mattermost/mattermost-mobile/releases/tag/v2.40.0) \| [Changelog](./mobile-app-changelog#release-v2-40-0) \| [SBOM download](https://github.com/mattermost/mattermost-mobile/releases/download/v2.40.0/sbom-mattermost-mobile-v2.40.0.json) | Released: 2026-05-15
Support Ends: 2026-06-15 | [v11.7](./mattermost-v11-changelog#release-v11-7-extended-support-release), [v11.6](./mattermost-v11-changelog#release-v11-6-feature-release), [v11.5](./mattermost-v11-changelog#release-v11-5-feature-release), [v10.11](./mattermost-v10-changelog#release-v10-11-extended-support-release) | diff --git a/docs/main/product-overview/mattermost-server-releases.mdx b/docs/main/product-overview/mattermost-server-releases.mdx index d0925699722e..d791442fd6aa 100644 --- a/docs/main/product-overview/mattermost-server-releases.mdx +++ b/docs/main/product-overview/mattermost-server-releases.mdx @@ -17,10 +17,10 @@ Mattermost releases a new server version on the 16th of each month in [binary fo | **Release** | **Released on** | **Support ends** | |:---|:---|:---| -| v11.10 [Download](https://releases.mattermost.com/11.10.0/mattermost-11.10.0-linux-amd64.tar.gz) \| [Changelog](./mattermost-v11-changelog#release-v11-10-feature-release) \|
SBOM
| 2026-08-14 | 2026-11-15 | +| v11.10 [Download](https://releases.mattermost.com/11.10.1/mattermost-11.10.1-linux-amd64.tar.gz) \| [Changelog](./mattermost-v11-changelog#release-v11-10-feature-release) \|
SBOM
| 2026-08-14 | 2026-11-15 | | v11.9 [Download](https://releases.mattermost.com/11.9.1/mattermost-11.9.1-linux-amd64.tar.gz) \| [Changelog](./mattermost-v11-changelog#release-v11-9-feature-release) \|
SBOM
| 2026-07-16 | 2026-10-15 | | v11.8 [Download](https://releases.mattermost.com/11.8.5/mattermost-11.8.5-linux-amd64.tar.gz) \| [Changelog](./mattermost-v11-changelog#release-v11-8-feature-release) \|
SBOM
| 2026-06-16 | 2026-09-15 | -| v11.7 [Download](https://releases.mattermost.com/11.7.9/mattermost-11.7.9-linux-amd64.tar.gz) \| [Changelog](./mattermost-v11-changelog#release-v11-7-extended-support-release) \|
SBOM
| 2026-05-15 | 2027-05-15 [EXTENDED](./release-policy#release-types) | +| v11.7 [Download](https://releases.mattermost.com/11.7.10/mattermost-11.7.10-linux-amd64.tar.gz) \| [Changelog](./mattermost-v11-changelog#release-v11-7-extended-support-release) \|
SBOM
| 2026-05-15 | 2027-05-15 [EXTENDED](./release-policy#release-types) | | v11.6 [Download](https://releases.mattermost.com/11.6.6/mattermost-11.6.6-linux-amd64.tar.gz) \| [Changelog](./mattermost-v11-changelog#release-v11-6-feature-release) \|
SBOM
| 2026-04-16 | 2026-07-15 | | v11.5 [Download](https://releases.mattermost.com/11.5.7/mattermost-11.5.7-linux-amd64.tar.gz) \| [Changelog](./mattermost-v11-changelog#release-v11-5-feature-release) \|
SBOM
| 2026-03-16 | 2026-06-15 | | v11.4 [Download](https://releases.mattermost.com/11.4.5/mattermost-11.4.5-linux-amd64.tar.gz) \| [Changelog](./mattermost-v11-changelog#release-v11-4-feature-release) \|
SBOM
| 2026-02-16 | 2026-05-15 | diff --git a/docs/main/product-overview/mattermost-v11-changelog.mdx b/docs/main/product-overview/mattermost-v11-changelog.mdx index 89befa5b3ed3..29f571d2161a 100644 --- a/docs/main/product-overview/mattermost-v11-changelog.mdx +++ b/docs/main/product-overview/mattermost-v11-changelog.mdx @@ -16,7 +16,31 @@ Platform and OS scope reflects reported and tested environments and may not repr ## Release v11.10 - [Feature Release](https://docs.mattermost.com/product-overview/release-policy.html#release-types) \{#release-v11-10-feature-release} -**Release Day: 2026-08-14** + + +**Breaking Changes** + - Starting with v11.10.1, the updated `glibc-openssl-fips` bundle includes a new OpenSSL build that enforces the FIPS minimum key length of 112 bits for HMAC operations. The PostgreSQL driver (lib/pq) passes the database password as the HMAC key during SCRAM authentication, so a password shorter than 112 bits, or 14 ASCII characters, now causes a panic on connect rather than a graceful failure. Before upgrading, FIPS deployments should verify that the password in `SqlSettings.DataSource` is at least 14 characters and rotate it in PostgreSQL if it is shorter. Standard, non-FIPS builds are unaffected. + + + +- **11.10.1, released 2026-08-25** + - Mattermost v11.10.1 contains medium to high severity level security fixes. [Upgrading](https://docs.mattermost.com/upgrade/upgrading-mattermost-server.html) to this release is recommended. Details will be posted on our [security updates page](https://mattermost.com/security-updates/) 30 days after release as per the [Mattermost Responsible Disclosure Policy](https://mattermost.com/security-vulnerability-report/). + - Go runtime was updated from 1.26.4 to 1.26.7. + - Fixed an issue where the **Classification Markings** section in the **System Console** was accessible and configurable under an Enterprise license; it is now correctly gated behind an Enterprise Advanced license and shows an upgrade prompt for lower tiers. + - Fixed an issue where a single video attachment was hidden from a post when image previews were set to collapsed, leaving no filename, thumbnail, or expand control. + - Updated `getFile` API validation to check for content reviewer earlier. + - Updated Slack import to handle user matching differently depending on the import type. + - Updated the local image proxy to enforce a maximum size limit when fetching images directly. + - Added signing/verification for RelayState in SAML flow. + - Updated how team admin status is assigned when a user joins a team. + - Prevented a post belonging to Direct/Group Message from being quarantined. + - Updated how post and thread payloads handle interactive-message action data. + - Fixed an edge case in team invitation handling. + - Fixed an issue where certain role update requests for channel and team members were not fully validated. + - Enhanced team search to only display teams relevant and accessible to the requesting user. + - Updated account type switch handling. +- **11.10.0, released 2026-08-14** + - Original 11.10.0 release. ### Upgrade Impact @@ -565,6 +589,33 @@ See [this blog post](https://mattermost.com/blog/mattermost-v11-8-0-is-now-avail ## Release v11.7 - [Extended Support Release](https://docs.mattermost.com/product-overview/release-policy.html#release-types) \{#release-v11-7-extended-support-release} + + +**Breaking Changes** + - Starting with v11.7.10, the updated `glibc-openssl-fips` bundle includes a new OpenSSL build that enforces the FIPS minimum key length of 112 bits for HMAC operations. The PostgreSQL driver (lib/pq) passes the database password as the HMAC key during SCRAM authentication, so a password shorter than 112 bits, or 14 ASCII characters, now causes a panic on connect rather than a graceful failure. Before upgrading, FIPS deployments should verify that the password in `SqlSettings.DataSource` is at least 14 characters and rotate it in PostgreSQL if it is shorter. Standard, non-FIPS builds are unaffected. + + + +- **11.7.10, released 2026-08-26** + - Mattermost v11.7.10 contains low to high severity level security fixes. [Upgrading](https://docs.mattermost.com/upgrade/upgrading-mattermost-server.html) to this release is recommended. Details will be posted on our [security updates page](https://mattermost.com/security-updates/) 30 days after release as per the [Mattermost Responsible Disclosure Policy](https://mattermost.com/security-vulnerability-report/). + - Go runtime was updated from 1.25.10 to 1.26.7. + - Pre-packaged Agents plugin version [v2.0.7](https://github.com/mattermost/mattermost-plugin-agents/releases/tag/v2.0.7). + - Updated `getFile` API validation to check for content reviewer earlier. + - Updated Slack import to handle user matching differently depending on the import type. + - Updated the local image proxy to enforce a maximum size limit when fetching images directly. + - Added signing/verification for RelayState in SAML flow. + - Updated how team admin status is assigned when a user joins a team. + - Prevented a post belonging to Direct/Group Message from being quarantined. + - Updated how post and thread payloads handle interactive-message action data. + - Fixed an edge case in team invitation handling. + - Fixed an issue where certain role update requests for channel and team members were not fully validated. + - Enhanced team search to only display teams relevant and accessible to the requesting user. + - Updated account type switch handling. + - Stopped desktop notification content from being used as the Web Notifications API tag, which could copy message bodies into endpoint telemetry on Chromium-based browsers. + - Fixed an issue where reactions using emoji names with mixed case (e.g. `:Mattermost:`) could not be added or removed. + - Fixed most of the layout shift caused by images in posts. + - The server now logs an error instead of refusing to start when it detects an unsupported PostgreSQL, Elasticsearch, or OpenSearch version. + - Mattermost v11.7.10 contains no database or functional changes. - **11.7.9, released 2026-08-13** - Mattermost v11.7.9 contains a medium severity level security fix. [Upgrading](https://docs.mattermost.com/upgrade/upgrading-mattermost-server.html) to this release is recommended. Details will be posted on our [security updates page](https://mattermost.com/security-updates/) 30 days after release as per the [Mattermost Responsible Disclosure Policy](https://mattermost.com/security-vulnerability-report/). - Fixed a memory leak due to Elasticsearch starting bulk indexers and not stopping them, leading to an Out Of Memory. Added logging to alert System Admins when they are missing the required ``analysis-icu`` Elasticsearch/OS plugin. diff --git a/docs/main/product-overview/mobile-app-changelog.mdx b/docs/main/product-overview/mobile-app-changelog.mdx index b688a4d22466..358c567b6641 100644 --- a/docs/main/product-overview/mobile-app-changelog.mdx +++ b/docs/main/product-overview/mobile-app-changelog.mdx @@ -16,6 +16,22 @@ Platform and OS scope reflects reported and tested environments and may not repr +## 2.43.1 Release \{#release-v2-43-1} + - Release Date: August 24, 2026 + - Server Versions Supported: Server v11.7.0+ is required. Self-Signed SSL certificates are not supported unless the user installs the CA certificate on their device. + +### Compatibility + - **Upgrade to server version v11.7.0 or later is required.** Support for server [Extended Support Release](https://docs.mattermost.com/product-overview/release-policy.html#extended-support-releases) (ESR) v10.11.0 has ended and upgrading to server ESR v11.7.0 or later is required. As we innovate and offer newer versions of our mobile apps, we maintain backwards compatibility only with supported server versions. Users who upgrade to the newest mobile apps while being connected to an unsupported server version can be exposed to compatibility issues, which can cause crashes or severe bugs that break core functionality of the app. + - Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html). + - iPhone 8+ devices and later with iOS 16.0+ are [required](https://support.apple.com/en-il/guide/iphone/iphe3fa5df43/16.0/ios/16.0). + +### Bug Fixes + - Fixed an issue with switching audio output on the Phone call interface and made sure that audio routing follows the user expectations. + +### Known Issues + - Users are unable to adjust the font size via the OS font size setting. + - Some Google Pixel phones on Android 12+ might not continue past the login screen. This is a known issue with the OS, and the current workaround is to restart the device. + ## 2.43.0 Release \{#release-v2-43-0} - Release Date: August 14, 2026 - Server Versions Supported: Server v11.7.0+ is required. Self-Signed SSL certificates are not supported unless the user installs the CA certificate on their device. diff --git a/docs/main/product-overview/release-policy.mdx b/docs/main/product-overview/release-policy.mdx index a52d0b8ccf10..7d7d4993aa7a 100644 --- a/docs/main/product-overview/release-policy.mdx +++ b/docs/main/product-overview/release-policy.mdx @@ -73,7 +73,7 @@ gantt axisFormat %b %y section Releases - v10.11 & Desktop App v5.13 Extended Support :crit, 2025-08-16, 2026-08-15 + v10.11 & Desktop App v5.13 Extended Support :crit, :done, 2025-08-16, 2026-08-15 v10.12 :done, 2025-09-16, 2025-12-15 v11.0 :done, 2025-10-16, 2026-01-15 v11.1 :done, 2025-11-14, 2026-02-15 @@ -82,7 +82,7 @@ gantt v11.4 :done, 2026-02-16, 2026-05-15 v11.5 :done, 2026-03-16, 2026-06-15 v11.6 :done, 2026-04-16, 2026-07-15 - v11.7 & Desktop App v6.2 Extended Support :crit, 2026-05-15, 2027-05-15 + v11.7 & Desktop App v6.2 Extended Support :crit, :active, 2026-05-15, 2027-05-15 v11.8 :active, 2026-06-16, 2026-09-15 v11.9 :active, 2026-07-16, 2026-10-15 v11.10 :active, 2026-08-14, 2026-11-15 diff --git a/docs/main/product-overview/version-archive.mdx b/docs/main/product-overview/version-archive.mdx index 3d2137fce3e4..6beb96eae585 100644 --- a/docs/main/product-overview/version-archive.mdx +++ b/docs/main/product-overview/version-archive.mdx @@ -19,12 +19,12 @@ Our package signing key has been moved away from Keybase. If you still reference -Mattermost Enterprise Edition v11.10.0 - [View Changelog](https://docs.mattermost.com/product-overview/mattermost-v11-changelog.html#release-v11-10-feature-release) - [Download](https://releases.mattermost.com/11.10.0/mattermost-11.10.0-linux-amd64.tar.gz?src=arc) +Mattermost Enterprise Edition v11.10.1 - [View Changelog](https://docs.mattermost.com/product-overview/mattermost-v11-changelog.html#release-v11-10-feature-release) - [Download](https://releases.mattermost.com/11.10.1/mattermost-11.10.1-linux-amd64.tar.gz?src=arc) -- `https://releases.mattermost.com/11.10.0/mattermost-11.10.0-linux-amd64.tar.gz` -- SHA-256 Checksum: `8ae404ed0c0fbab2b01c00c031b9d0f3e17c51007e3bb3401bebaf3e60da0c3e` -- GPG Signature: [https://releases.mattermost.com/11.10.0/mattermost-11.10.0-linux-amd64.tar.gz.sig](https://releases.mattermost.com/11.10.0/mattermost-11.10.0-linux-amd64.tar.gz.sig) -- SBOM Download Link: [https://releases.mattermost.com/11.10.0/sbom-enterprise-v11.10.0.json](https://releases.mattermost.com/11.10.0/sbom-enterprise-v11.10.0.json) +- `https://releases.mattermost.com/11.10.1/mattermost-11.10.1-linux-amd64.tar.gz` +- SHA-256 Checksum: `43259570cc109c674f18db5b4a2b9f5a0b5dcd0eead65ef6df62bf286333e8de` +- GPG Signature: [https://releases.mattermost.com/11.10.1/mattermost-11.10.1-linux-amd64.tar.gz.sig](https://releases.mattermost.com/11.10.1/mattermost-11.10.1-linux-amd64.tar.gz.sig) +- SBOM Download Link: [https://releases.mattermost.com/11.10.1/sbom-enterprise-v11.10.1.json](https://releases.mattermost.com/11.10.1/sbom-enterprise-v11.10.1.json) Mattermost Enterprise Edition v11.9.1 - [View Changelog](https://docs.mattermost.com/product-overview/mattermost-v11-changelog.html#release-v11-9-feature-release) - [Download](https://releases.mattermost.com/11.9.1/mattermost-11.9.1-linux-amd64.tar.gz?src=arc) @@ -40,12 +40,12 @@ Mattermost Enterprise Edition v11.8.5 - [View Changelog](https://docs.mattermost - GPG Signature: [https://releases.mattermost.com/11.8.5/mattermost-11.8.5-linux-amd64.tar.gz.sig](https://releases.mattermost.com/11.8.5/mattermost-11.8.5-linux-amd64.tar.gz.sig) - SBOM Download Link: [https://releases.mattermost.com/11.8.5/sbom-enterprise-v11.8.5.json](https://releases.mattermost.com/11.8.5/sbom-enterprise-v11.8.5.json) -Mattermost Enterprise Edition v11.7.9 *Extended Support Release (ESR)* - [View Changelog](https://docs.mattermost.com/product-overview/mattermost-v11-changelog.html#release-v11-7-extended-support-release) - [Download](https://releases.mattermost.com/11.7.9/mattermost-11.7.9-linux-amd64.tar.gz?src=arc) +Mattermost Enterprise Edition v11.7.10 *Extended Support Release (ESR)* - [View Changelog](https://docs.mattermost.com/product-overview/mattermost-v11-changelog.html#release-v11-7-extended-support-release) - [Download](https://releases.mattermost.com/11.7.10/mattermost-11.7.10-linux-amd64.tar.gz?src=arc) -- `https://releases.mattermost.com/11.7.9/mattermost-11.7.9-linux-amd64.tar.gz` -- SHA-256 Checksum: `a22e631ae4a1f704c2bcf45ad8dcc377184081c50954a9269b7f62bd663a7a04` -- GPG Signature: [https://releases.mattermost.com/11.7.9/mattermost-11.7.9-linux-amd64.tar.gz.sig](https://releases.mattermost.com/11.7.9/mattermost-11.7.9-linux-amd64.tar.gz.sig) -- SBOM Download Link: [https://releases.mattermost.com/11.7.9/sbom-enterprise-v11.7.9.json](https://releases.mattermost.com/11.7.9/sbom-enterprise-v11.7.9.json) +- `https://releases.mattermost.com/11.7.10/mattermost-11.7.10-linux-amd64.tar.gz` +- SHA-256 Checksum: `bb66b6f7ddd310e4cede39b256a2f35b825347045a05bb14eaea90081381e270` +- GPG Signature: [https://releases.mattermost.com/11.7.10/mattermost-11.7.10-linux-amd64.tar.gz.sig](https://releases.mattermost.com/11.7.10/mattermost-11.7.10-linux-amd64.tar.gz.sig) +- SBOM Download Link: [https://releases.mattermost.com/11.7.10/sbom-enterprise-v11.7.10.json](https://releases.mattermost.com/11.7.10/sbom-enterprise-v11.7.10.json) Mattermost Enterprise Edition v11.6.6 - [View Changelog](https://docs.mattermost.com/product-overview/mattermost-v11-changelog.html#release-v11-6-feature-release) - [Download](https://releases.mattermost.com/11.6.6/mattermost-11.6.6-linux-amd64.tar.gz?src=arc) - `https://releases.mattermost.com/11.6.6/mattermost-11.6.6-linux-amd64.tar.gz` @@ -632,12 +632,12 @@ The open source Mattermost Team Edition is functionally identical to the commerc We generally recommend installing Enterprise Edition, even if you don't currently need a license. This provides the flexibility to seamlessly unlock Enterprise features should you need them. However, if you only want to install software with a fully open source code base, then Team Edition is the best choice for you. -Mattermost Team Edition v11.10.0 - [View Changelog](https://docs.mattermost.com/product-overview/mattermost-v11-changelog.html#release-v11-10-feature-release) - [Download](https://releases.mattermost.com/11.10.0/mattermost-team-11.10.0-linux-amd64.tar.gz?src=arc) +Mattermost Team Edition v11.10.1 - [View Changelog](https://docs.mattermost.com/product-overview/mattermost-v11-changelog.html#release-v11-10-feature-release) - [Download](https://releases.mattermost.com/11.10.1/mattermost-team-11.10.1-linux-amd64.tar.gz?src=arc) -- `https://releases.mattermost.com/11.10.0/mattermost-team-11.10.0-linux-amd64.tar.gz` -- SHA-256 Checksum: `376f4be1fcd83bd65ab3f4dc580ba87684e4caed93d357e965b95703854f7c05` -- GPG Signature: [https://releases.mattermost.com/11.10.0/mattermost-team-11.10.0-linux-amd64.tar.gz.sig](https://releases.mattermost.com/11.10.0/mattermost-team-11.10.0-linux-amd64.tar.gz.sig) -- SBOM Download Link: [https://github.com/mattermost/mattermost/releases/download/v11.10.0/sbom-mattermost-v11.10.0.json](https://github.com/mattermost/mattermost/releases/download/v11.10.0/sbom-mattermost-v11.10.0.json) +- `https://releases.mattermost.com/11.10.1/mattermost-team-11.10.1-linux-amd64.tar.gz` +- SHA-256 Checksum: `df0c8fe41c1dbae8110be71d0db40bccdba17120e3e30da20ab1dee8b9354503` +- GPG Signature: [https://releases.mattermost.com/11.10.1/mattermost-team-11.10.1-linux-amd64.tar.gz.sig](https://releases.mattermost.com/11.10.1/mattermost-team-11.10.1-linux-amd64.tar.gz.sig) +- SBOM Download Link: [https://github.com/mattermost/mattermost/releases/download/v11.10.1/sbom-mattermost-v11.10.1.json](https://github.com/mattermost/mattermost/releases/download/v11.10.1/sbom-mattermost-v11.10.1.json) Mattermost Team Edition v11.9.1 - [View Changelog](https://docs.mattermost.com/product-overview/mattermost-v11-changelog.html#release-v11-9-feature-release) - [Download](https://releases.mattermost.com/11.9.1/mattermost-team-11.9.1-linux-amd64.tar.gz?src=arc) @@ -653,12 +653,12 @@ Mattermost Team Edition v11.8.5 - [View Changelog](https://docs.mattermost.com/p - GPG Signature: [https://releases.mattermost.com/11.8.5/mattermost-team-11.8.5-linux-amd64.tar.gz.sig](https://releases.mattermost.com/11.8.5/mattermost-team-11.8.5-linux-amd64.tar.gz.sig) - SBOM Download Link: [https://github.com/mattermost/mattermost/releases/download/v11.8.5/sbom-mattermost-v11.8.5.json](https://github.com/mattermost/mattermost/releases/download/v11.8.5/sbom-mattermost-v11.8.5.json) -Mattermost Team Edition v11.7.9 *Extended Support Release (ESR)* - [View Changelog](https://docs.mattermost.com/product-overview/mattermost-v11-changelog.html#release-v11-7-extended-support-release) - [Download](https://releases.mattermost.com/11.7.9/mattermost-team-11.7.9-linux-amd64.tar.gz?src=arc) +Mattermost Team Edition v11.7.10 *Extended Support Release (ESR)* - [View Changelog](https://docs.mattermost.com/product-overview/mattermost-v11-changelog.html#release-v11-7-extended-support-release) - [Download](https://releases.mattermost.com/11.7.10/mattermost-team-11.7.10-linux-amd64.tar.gz?src=arc) -- `https://releases.mattermost.com/11.7.9/mattermost-team-11.7.9-linux-amd64.tar.gz` -- SHA-256 Checksum: `df9b76ee40212c1552162486befc80f52ba9eefbde1ac6a490bdda4188ba54f0` -- GPG Signature: [https://releases.mattermost.com/11.7.9/mattermost-team-11.7.9-linux-amd64.tar.gz.sig](https://releases.mattermost.com/11.7.9/mattermost-team-11.7.9-linux-amd64.tar.gz.sig) -- SBOM Download Link: [https://github.com/mattermost/mattermost/releases/download/v11.7.9/sbom-mattermost-v11.7.9.json](https://github.com/mattermost/mattermost/releases/download/v11.7.9/sbom-mattermost-v11.7.9.json) +- `https://releases.mattermost.com/11.7.10/mattermost-team-11.7.10-linux-amd64.tar.gz` +- SHA-256 Checksum: `94a86753ecdc3bcbbc61d214ca3a2070d9b61d5ab6b02c6c2fed4f7e321259ec` +- GPG Signature: [https://releases.mattermost.com/11.7.10/mattermost-team-11.7.10-linux-amd64.tar.gz.sig](https://releases.mattermost.com/11.7.10/mattermost-team-11.7.10-linux-amd64.tar.gz.sig) +- SBOM Download Link: [https://github.com/mattermost/mattermost/releases/download/v11.7.10/sbom-mattermost-v11.7.10.json](https://github.com/mattermost/mattermost/releases/download/v11.7.10/sbom-mattermost-v11.7.10.json) Mattermost Team Edition v11.6.6 - [View Changelog](https://docs.mattermost.com/product-overview/mattermost-v11-changelog.html#release-v11-6-feature-release) - [Download](https://releases.mattermost.com/11.6.6/mattermost-team-11.6.6-linux-amd64.tar.gz?src=arc) - `https://releases.mattermost.com/11.6.6/mattermost-team-11.6.6-linux-amd64.tar.gz` diff --git a/docs/main/use-case-guide/maximize-microsoft-investments.mdx b/docs/main/use-case-guide/maximize-microsoft-investments.mdx index 6afeeaee9364..47badb9c6158 100644 --- a/docs/main/use-case-guide/maximize-microsoft-investments.mdx +++ b/docs/main/use-case-guide/maximize-microsoft-investments.mdx @@ -18,18 +18,6 @@ Agencies and critical infrastructure organizations must often comply with strict - **Enable secure Microsoft Teams interactions via embedded Mattermost collaboration**, supporting operations within familiar interfaces while enforcing regulatory compliance. See [Mattermost for M365, Teams, and Outlook](/integrations-guide/mattermost-mission-collaboration-for-m365). - **Enforce agency-specific policies** with [legal hold](/administration-guide/comply/legal-hold), [retention policies](/administration-guide/comply/data-retention-policy), and [user access controls](/administration-guide/onboard/advanced-permissions) that align with national or sectoral mandates. -## On-Premises Skype for Business Replacement - -As Skype for Business reaches end-of-life, secure organizations require an alternative that preserves on-premises control, integrates into Microsoft workflows, and meets the security standards of air-gapped and classified environments. Cloud-first replacements like Microsoft Teams are not always viable due to network segmentation, compliance restrictions, or data sovereignty mandates. - -![Extend Microsoft Enterprise IT investments for edge-based, highly tailored Mission IT workflows with Mattermost.](/images/On-Prem-Skype-for-Business-replace.png) - -- **Preserve mission-critical communication workflows** with a self-hosted Mattermost deployment that supports [1:1 calls](/end-user-guide/collaborate/make-calls), [screen sharing](/end-user-guide/collaborate/make-calls#share-your-screen), and [threaded messaging](/end-user-guide/collaborate/organize-conversations) within secure environments. -- **Integrate Mattermost with Microsoft tools** such as Outlook, Teams, and [Entra ID Single Sign-On](/administration-guide/onboard/sso-entraid) to retain user workflows while centralizing identity and access control. See [Mattermost for M365, Teams, and Outlook](/integrations-guide/mattermost-mission-collaboration-for-m365). -- **Deploy in sovereign, air-gapped, or private cloud environments** such as [Azure Deployment](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/mattermost.mattermost-operator?tab=overview) or **Azure Local** (formerly Azure Stack HCI) for on-premises hybrid cloud scenarios while maintaining compliance with STIG, FedRAMP, and NIST 800-53 standards. For Azure Local deployments, we recommend engaging **Mattermost Professional Services** for deployment support. [Talk to an Expert](https://mattermost.com/contact-sales/) to learn more. - -[Learn more](/use-case-guide/on-prem-skype-for-business-replacement) about replacing Skype for Business with Mattermost. - ## Out-of-Band Incident Response for Microsoft-Centric Environments During high-stakes incidents, Microsoft 365 tools can be limited or unavailable, slowing down response times and jeopardizing mission continuity. diff --git a/docs/main/use-case-guide/on-prem-skype-for-business-replacement.mdx b/docs/main/use-case-guide/on-prem-skype-for-business-replacement.mdx deleted file mode 100644 index bef480d09879..000000000000 --- a/docs/main/use-case-guide/on-prem-skype-for-business-replacement.mdx +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: "On-Premises Skype for Business Replacement" ---- -With Skype for Business reaching end-of-life, security-conscious organizations face a critical inflection point. Many operate in air-gapped or classified environments where cloud-based alternatives are not viable due to compliance restrictions, risk exposure, or data sovereignty mandates. Without a secure, modern, on-premises collaboration platform, these organizations risk operational disruption, mission misalignment, and non-compliance with stringent regulatory frameworks. - -Mattermost provides a secure, self-hosted communication and collaboration platform purpose-built for air-gapped environments, classified networks, and regulated industries. Designed to meet NIST 800-53, FedRAMP, and DISA STIG compliance requirements, Mattermost replaces legacy tools with modern capabilities—secure messaging, file sharing, workflow automation, and integrated video collaboration—while maintaining full enterprise control. Organizations can operate at scale, enable external collaboration without policy violations, and modernize their digital workflows without compromising security. - -![Extend Microsoft Enterprise IT investments for edge-based, highly tailored Mission IT workflows with Mattermost.](/images/On-Prem-Skype-for-Business-replace.png) - -The following mission-ready collaboration capabilities are available: - -## Air-Gapped and Classified Operations - -Organizations operating in fully disconnected or classified environments require secure communication platforms that function entirely within their own infrastructure. - -**Benefits** - -- **Ensure secure communication in fully disconnected networks** using Mattermost's support for private on-premise deployments, including FIPS 140-3 validated and DISA STIG-hardened container images. [Learn more](/deployment-guide/reference-architecture/application-architecture) about Mattermost's architecture, components, and backend infrastructure. -- **Maintain operational continuity** with enterprise-grade [channel-based collaboration](/end-user-guide/messaging-collaboration)— including [1:1 audio calls](/end-user-guide/collaborate/make-calls), [screen sharing](/end-user-guide/collaborate/make-calls#share-your-screen), [threaded messaging](/end-user-guide/collaborate/organize-conversations), and [file sharing](/end-user-guide/collaborate/share-files-in-messages)—entirely within air-gapped systems. -- **Scale to mission requirements** with a [high-availability, horizontally scalable architecture](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) that supports tens of thousands of users in secure on-prem environments. -- **Preserve data sovereignty and eliminate external dependencies** with a self-hosted [Kubernetes deployment model](/deployment-guide/server/deploy-kubernetes) that integrates into classified networks, sovereign data centers, or **Azure Local** (formerly Azure Stack HCI) for hybrid cloud on-premises scenarios. - -## Modernize Secure Collaboration Workflows - -Legacy communication tools lack the flexibility, automation, and usability demanded by modern operational teams. Mattermost introduces modern collaboration workflows without compromising compliance or deployment control. - -**Benefits** - -- **Enable dynamic, cross-platform messaging and coordination** with a unified interface across web, desktop, and mobile—featuring [threaded discussions](/end-user-guide/collaborate/organize-conversations), [file previews](/end-user-guide/collaborate/share-files-in-messages#preview-file-attachments), and [screen sharing](/end-user-guide/collaborate/make-calls#share-your-screen). -- **Streamline mission-critical processes** with [Collaborative Playbooks](/end-user-guide/workflow-automation) that automate and track workflows like incident response, shift turnover, and logistics planning. -- **Embed secure video conferencing into daily operations** using the [Pexip integration](https://mattermost.com/marketplace/pexip-video-connect/), allowing real-time video engagement from within your air-gapped or secure infrastructure. -- **Support operational task management** through optional Kanban-style [Boards](https://github.com/mattermost/mattermost-plugin-boards) for structured, accountable planning—hosted securely within your own network. -- **Align the user experience with your operational identity** using [custom branding](/administration-guide/configure/custom-branding-tools), [theming](/end-user-guide/preferences/customize-your-theme), and [product localization](/end-user-guide/preferences/manage-your-display-options#language) across more than 20 languages to support multinational teams. - -## Enterprise-Controlled External Collaboration - -Collaborating across organizational boundaries must not compromise compliance or IT governance. Mattermost enables secure external engagement while keeping control centralized within the enterprise. - -![Mattermost replaces Signal, Discord and other free personal apps with secure external messaging controlled by IT.](/images/External-Collaboration-with-Enterprise-Control.png) - -**Benefits** - -- **Collaborate securely with third parties** via Connected Workspaces that allow messaging, [file sharing](/end-user-guide/collaborate/share-files-in-messages), and [thread-based discussions](/end-user-guide/collaborate/organize-conversations) with external teams—without exposing internal systems. -- **Apply fine-grained access controls and retention policies** to external users through enterprise-managed [permissions](/administration-guide/onboard/advanced-permissions), [audit logging](/administration-guide/manage/logging#audit-logging), and [channel-specific configurations](/administration-guide/manage/team-channel-members#advanced-access-controls). -- **Integrate with Microsoft Teams, Exchange, and M365** to maintain centralized workflows and extend secure communication to external stakeholders without leaving policy-aligned platforms. See [Mattermost for M365, Teams, and Outlook](/integrations-guide/mattermost-mission-collaboration-for-m365). -- **Manage user identity and access** across internal and external roles using Microsoft [Entra ID](/administration-guide/onboard/sso-entraid) (Azure AD) synchronization for scalable and compliant provisioning. - -## Get Started - -With Mattermost, your organization gains a self-hosted, scalable, and compliant solution tailored for classified operations, secure external engagement, and operational modernization. - -[Talk to an Expert](https://mattermost.com/contact-sales/) to learn more about transitioning from Skype for Business to a secure, modern collaboration platform built for mission-critical environments, or to discuss your Azure Local deployment needs. Organizations deploying Mattermost on Azure Local (formerly Azure Stack HCI) for on-premises hybrid cloud scenarios can engage **Mattermost Professional Services** for deployment support to ensure optimal configuration and compliance with your security requirements. diff --git a/docs/main/use-case-guide/self-sovereign-collaboration.mdx b/docs/main/use-case-guide/self-sovereign-collaboration.mdx index 74fd113add07..07dd277aeb3d 100644 --- a/docs/main/use-case-guide/self-sovereign-collaboration.mdx +++ b/docs/main/use-case-guide/self-sovereign-collaboration.mdx @@ -40,7 +40,6 @@ Cross-agency, multinational, or coalition collaboration requires sovereignty wit - **Create secure shared workspaces** with [Connected Workspaces Channels](/administration-guide/onboard/connected-workspaces) that synchronize discussions, reactions, and file sharing across trusted organizations—without exposing internal systems. - **Control access across organizations** with [attribute-based permissions](/administration-guide/onboard/advanced-permissions) and scoped identity policies to ensure mission alignment and sensitive information segmentation. - **Deploy sovereign AI and workflow automation** in isolated environments using [air-gapped AI operations](/end-user-guide/agents) and [Collaborative Playbooks](/end-user-guide/workflow-automation)—enabling intelligence and speed without compromising data control. -- **Upgrade legacy platforms** like Skype for Business with modern, compliant tools for secure messaging, screen sharing, and team coordination. [See Skype for Business replacement options](/use-case-guide/on-prem-skype-for-business-replacement). ## Unified Collaboration for Secure Workflows diff --git a/docs/main/use-case-guide/use-cases-index.mdx b/docs/main/use-case-guide/use-cases-index.mdx index fb871656ca53..0db27b7a4ddf 100644 --- a/docs/main/use-case-guide/use-cases-index.mdx +++ b/docs/main/use-case-guide/use-cases-index.mdx @@ -6,7 +6,6 @@ Learn how operational teams use Mattermost to accelerate mission-critical work a - [Integrated Security Operations](/use-case-guide/integrated-security-operations) - Accelerate detection, decision-making, and coordinated response while maintaining full operational control in Security Operations Centers (SOCs), red team engagements, CERT responses, and cross-organizational intelligence hubs. - [Maximize your Microsoft investments](/use-case-guide/maximize-microsoft-investments) - Speed mission-critical outcomes by supplementing existing investments in Microsoft Teams, M365, and Entra ID for everyday collaboration with Mattermost's specialized workflow platform for technical and operational teams needing advanced customization, toolchain integration, and deployment to segregated networks. - [Mission-Ready Mobile](/use-case-guide/mission-ready-mobile) - Secure mobile collaboration for defense, law enforcement, and public sector operations optimized for low-bandwidth and disconnected conditions with protections including ID-only push notifications, biometric authentication, jailbreak detection, and full MDM/EMM support. -- [On-Premises Skype for Business replacement](/use-case-guide/on-prem-skype-for-business-replacement) - Replace Skype for Business with Mattermost in classified operations. - [Out-of-Band Incident Response](/use-case-guide/out-of-band-incident-response) - Ensure real-time coordination remains available during network outages, security incidents, or critical decision windows when primary communication channels are unavailable. - [Purpose-Built Collaboration](/use-case-guide/purpose-built-collaboration) - Streamline coordination, improve decision velocity, and increase resilience across mission-critical workflows. - [Real-Time DevSecOps collaboration](/use-case-guide/devops-collaboration) - Support sovereign software supply chains, regulated platforms, or air-gapped operational environments to accelerate software development and deployment processes and reduce costs. diff --git a/docs/site/scripts/gen-documentation-sidebar.mjs b/docs/site/scripts/gen-documentation-sidebar.mjs index fcf2804fae4c..cbdbe8fc5a0c 100644 --- a/docs/site/scripts/gen-documentation-sidebar.mjs +++ b/docs/site/scripts/gen-documentation-sidebar.mjs @@ -366,6 +366,7 @@ const DEPLOYMENT_HIDDEN = new Set([ 'server/docker-troubleshooting', // → troubleshooting 'server/trouble_mysql', // → troubleshooting 'server/trouble-postgres', // → troubleshooting + 'server/fips-migration', // cross-linked only; sidebar home is the Migration hub (administration-guide/manage/admin/migration) ]); // --------------------------------------------------------------------------- diff --git a/docs/site/sidebars/active-redirects.json b/docs/site/sidebars/active-redirects.json index 548f3c91b460..99d4c3dd727d 100644 --- a/docs/site/sidebars/active-redirects.json +++ b/docs/site/sidebars/active-redirects.json @@ -149,7 +149,11 @@ }, { "from": "/about/on-prem-skype-for-business-replacement", - "to": "/use-case-guide/on-prem-skype-for-business-replacement" + "to": "/use-case-guide/use-cases-index" + }, + { + "from": "/use-case-guide/on-prem-skype-for-business-replacement", + "to": "/use-case-guide/use-cases-index" }, { "from": "/about/out-of-band-incident-response", diff --git a/docs/site/static/images/keycloak_1_client_signature_encryption.png b/docs/site/static/images/keycloak_1_client_signature_encryption.png index 2e80815a5442..bdce0757a533 100644 Binary files a/docs/site/static/images/keycloak_1_client_signature_encryption.png and b/docs/site/static/images/keycloak_1_client_signature_encryption.png differ diff --git a/server/channels/api4/properties.go b/server/channels/api4/properties.go index 5c70f85ab8d6..52bc5f484a7a 100644 --- a/server/channels/api4/properties.go +++ b/server/channels/api4/properties.go @@ -24,7 +24,8 @@ func (api *API) InitProperties() { api.srv.Config().FeatureFlags.ManagedChannelCategories || api.srv.Config().FeatureFlags.ClassificationMarkings || api.srv.Config().FeatureFlags.SessionAttributes || - api.srv.Config().FeatureFlags.PostAttributes { + api.srv.Config().FeatureFlags.PostAttributes || + api.srv.Config().FeatureFlags.ChannelAttributes { api.BaseRoutes.PropertyFields.Handle("", api.APISessionRequired(getPropertyFields)).Methods(http.MethodGet) api.BaseRoutes.PropertyFieldsSearch.Handle("", api.APISessionRequired(searchPropertyFields)).Methods(http.MethodPost) api.BaseRoutes.PropertyValues.Handle("", api.APISessionRequired(getPropertyValues)).Methods(http.MethodGet) diff --git a/server/channels/api4/properties_test.go b/server/channels/api4/properties_test.go index 80be4d6b34a8..fc210c3a8fdb 100644 --- a/server/channels/api4/properties_test.go +++ b/server/channels/api4/properties_test.go @@ -196,6 +196,99 @@ func TestPropertyRoutesWithPostAttributesFlag(t *testing.T) { }) } +// The gate is an OR across every Properties feature flag, so the off case is +// only meaningful with all of them disabled — ClassificationMarkings defaults to +// true. This is the half that backs "off by default exposes nothing". +func TestPropertyRoutesAbsentWithoutGateFlags(t *testing.T) { + mainHelper.Parallel(t) + + th := SetupConfig(t, func(cfg *model.Config) { + cfg.FeatureFlags.IntegratedBoards = false + cfg.FeatureFlags.ManagedChannelCategories = false + cfg.FeatureFlags.ClassificationMarkings = false + cfg.FeatureFlags.SessionAttributes = false + cfg.FeatureFlags.PostAttributes = false + cfg.FeatureFlags.ChannelAttributes = false + }).InitBasic(t) + + group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{ + Name: "channel_attributes_off_test", + Version: model.PropertyGroupVersionV2, + }) + require.Nil(t, appErr) + + _, resp, err := th.SystemAdminClient.GetPropertyFields(context.Background(), group.Name, "channel", model.PropertyFieldSearch{TargetType: "system"}) + require.Error(t, err) + CheckNotFoundStatus(t, resp) + + field := &model.PropertyField{ + Name: model.NewId(), + Type: model.PropertyFieldTypeText, + TargetType: "system", + } + _, resp, err = th.SystemAdminClient.CreatePropertyField(context.Background(), group.Name, "channel", field) + require.Error(t, err) + CheckNotFoundStatus(t, resp) + + // Patch a field that genuinely exists, created through the app layer since + // the create route is gated off (asserted above). A random field ID would + // 404 on its own and prove nothing about the route being absent. + memberLevel := model.PermissionLevelMember + existingField, appErr := th.App.CreatePropertyField(th.Context, &model.PropertyField{ + Name: model.NewId(), + Type: model.PropertyFieldTypeText, + GroupID: group.ID, + ObjectType: "channel", + TargetType: "system", + PermissionField: &memberLevel, + PermissionValues: &memberLevel, + PermissionOptions: &memberLevel, + }, false, "") + require.Nil(t, appErr) + + _, resp, err = th.SystemAdminClient.PatchPropertyValues(context.Background(), group.Name, "channel", th.BasicChannel.Id, []model.PropertyValuePatchItem{ + {FieldID: existingField.ID, Value: json.RawMessage(`"x"`)}, + }) + require.Error(t, err) + CheckNotFoundStatus(t, resp) +} + +func TestPropertyRoutesWithChannelAttributesFlag(t *testing.T) { + mainHelper.Parallel(t) + + th := SetupConfig(t, func(cfg *model.Config) { + cfg.FeatureFlags.IntegratedBoards = false + cfg.FeatureFlags.ManagedChannelCategories = false + cfg.FeatureFlags.ClassificationMarkings = false + cfg.FeatureFlags.SessionAttributes = false + cfg.FeatureFlags.PostAttributes = false + cfg.FeatureFlags.ChannelAttributes = true + }).InitBasic(t) + + group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{ + Name: "channel_attributes_test", + Version: model.PropertyGroupVersionV2, + }) + require.Nil(t, appErr) + require.NotNil(t, group) + + field := &model.PropertyField{ + Name: model.NewId(), + Type: model.PropertyFieldTypeText, + TargetType: "system", + } + createdField, resp, err := th.SystemAdminClient.CreatePropertyField(context.Background(), group.Name, "channel", field) + require.NoError(t, err) + CheckCreatedStatus(t, resp) + require.NotEmpty(t, createdField.ID) + + fields, resp, err := th.SystemAdminClient.GetPropertyFields(context.Background(), group.Name, "channel", model.PropertyFieldSearch{TargetType: "system"}) + require.NoError(t, err) + CheckOKStatus(t, resp) + require.Len(t, fields, 1) + require.Equal(t, createdField.ID, fields[0].ID) +} + func TestCreatePropertyField(t *testing.T) { mainHelper.Parallel(t) th := SetupConfig(t, func(cfg *model.Config) { @@ -3744,6 +3837,24 @@ func TestPatchPropertyValuesChannelTargetAccess(t *testing.T) { return createdField } + createAdminField := func(t *testing.T) *model.PropertyField { + t.Helper() + adminLevel := model.PermissionLevelAdmin + field := &model.PropertyField{ + Name: model.NewId(), + Type: model.PropertyFieldTypeText, + GroupID: group.ID, + ObjectType: "channel", + TargetType: "system", + PermissionField: &adminLevel, + PermissionValues: &adminLevel, + PermissionOptions: &adminLevel, + } + createdField, appErr := th.App.CreatePropertyField(th.Context, field, false, "") + require.Nil(t, appErr) + return createdField + } + // Create a non-member user nonMember := th.CreateUser(t) nonMemberClient := th.CreateClient() @@ -3824,6 +3935,19 @@ func TestPatchPropertyValuesChannelTargetAccess(t *testing.T) { CheckForbiddenStatus(t, resp) }) + t.Run("DM channel - system admin non-participant can write admin-tier value", func(t *testing.T) { + dmChannel := th.CreateDmChannel(t, th.BasicUser2) + f := createAdminField(t) + + items := []model.PropertyValuePatchItem{ + {FieldID: f.ID, Value: json.RawMessage(`"dm-admin-val"`)}, + } + values, resp, err := th.SystemAdminClient.PatchPropertyValues(context.Background(), group.Name, "channel", dmChannel.Id, items) + require.NoError(t, err) + CheckOKStatus(t, resp) + require.Len(t, values, 1) + }) + t.Run("GM channel - participant can write", func(t *testing.T) { gmChannel, appErr := th.App.CreateGroupChannel(th.Context, []string{th.BasicUser.Id, th.BasicUser2.Id, th.SystemAdminUser.Id}, th.BasicUser.Id) require.Nil(t, appErr) @@ -3851,6 +3975,188 @@ func TestPatchPropertyValuesChannelTargetAccess(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) }) + + t.Run("GM channel - system admin non-participant can write admin-tier value", func(t *testing.T) { + thirdUser := th.CreateUser(t) + gmChannel, appErr := th.App.CreateGroupChannel(th.Context, []string{th.BasicUser.Id, th.BasicUser2.Id, thirdUser.Id}, th.BasicUser.Id) + require.Nil(t, appErr) + f := createAdminField(t) + + items := []model.PropertyValuePatchItem{ + {FieldID: f.ID, Value: json.RawMessage(`"gm-admin-val"`)}, + } + values, resp, err := th.SystemAdminClient.PatchPropertyValues(context.Background(), group.Name, "channel", gmChannel.Id, items) + require.NoError(t, err) + CheckOKStatus(t, resp) + require.Len(t, values, 1) + }) +} + +// TestPatchPropertyValuesChannelAdminTier covers the two independent gates an +// admin-tier channel value write has to clear: the outer per-channel +// "may write properties here" check (manage_*_channel_properties, a channel_user +// grant) and the inner per-field tier (manage_channel_roles, a channel_admin +// grant that team_admin also carries). Clearing one never implies the other. +func TestPatchPropertyValuesChannelAdminTier(t *testing.T) { + mainHelper.Parallel(t) + // ChannelAttributes is the only enabled gate flag, so this also asserts it + // opens the channel *value* routes and not just the field routes. + th := SetupConfig(t, func(cfg *model.Config) { + cfg.FeatureFlags.IntegratedBoards = false + cfg.FeatureFlags.ManagedChannelCategories = false + cfg.FeatureFlags.ClassificationMarkings = false + cfg.FeatureFlags.SessionAttributes = false + cfg.FeatureFlags.PostAttributes = false + cfg.FeatureFlags.ChannelAttributes = true + }).InitBasic(t) + + group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{Name: "test_chan_admin_tier", Version: model.PropertyGroupVersionV2}) + require.Nil(t, appErr) + + adminLevel := model.PermissionLevelAdmin + + createAdminField := func(t *testing.T) *model.PropertyField { + t.Helper() + createdField, appErr := th.App.CreatePropertyField(th.Context, &model.PropertyField{ + Name: model.NewId(), + Type: model.PropertyFieldTypeText, + GroupID: group.ID, + ObjectType: "channel", + TargetType: "system", + PermissionField: &adminLevel, + PermissionValues: &adminLevel, + PermissionOptions: &adminLevel, + }, false, "") + require.Nil(t, appErr) + return createdField + } + + newChannel := func(t *testing.T, channelType model.ChannelType) *model.Channel { + t.Helper() + channel, appErr := th.App.CreateChannel(th.Context, &model.Channel{ + TeamId: th.BasicTeam.Id, + Type: channelType, + Name: model.NewId(), + DisplayName: "Channel Attributes Test", + CreatorId: th.SystemAdminUser.Id, + }, false) + require.Nil(t, appErr) + return channel + } + + loginNewClient := func(t *testing.T, user *model.User) *model.Client4 { + t.Helper() + client := th.CreateClient() + _, _, err := client.Login(context.Background(), user.Email, user.Password) + require.NoError(t, err) + return client + } + + t.Run("plain channel member cannot write an admin-tier value", func(t *testing.T) { + f := createAdminField(t) + th.LoginBasic(t) + + items := []model.PropertyValuePatchItem{ + {FieldID: f.ID, Value: json.RawMessage(`"member-attempt"`)}, + } + _, resp, err := th.Client.PatchPropertyValues(context.Background(), group.Name, "channel", th.BasicChannel.Id, items) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + }) + + t.Run("channel admin can write an admin-tier value", func(t *testing.T) { + channel := newChannel(t, model.ChannelTypeOpen) + channelAdmin := th.CreateUser(t) + th.LinkUserToTeam(t, channelAdmin, th.BasicTeam) + th.AddUserToChannel(t, channelAdmin, channel) + th.MakeUserChannelAdmin(t, channelAdmin, channel) + client := loginNewClient(t, channelAdmin) + + f := createAdminField(t) + items := []model.PropertyValuePatchItem{ + {FieldID: f.ID, Value: json.RawMessage(`"channel-admin-val"`)}, + } + values, resp, err := client.PatchPropertyValues(context.Background(), group.Name, "channel", channel.Id, items) + require.NoError(t, err) + CheckOKStatus(t, resp) + require.Len(t, values, 1) + require.Equal(t, json.RawMessage(`"channel-admin-val"`), values[0].Value) + }) + + t.Run("team admin inherits the admin tier inside a channel they belong to", func(t *testing.T) { + channel := newChannel(t, model.ChannelTypeOpen) + teamAdmin := th.CreateUser(t) + th.LinkUserToTeam(t, teamAdmin, th.BasicTeam) + th.UpdateUserToTeamAdmin(t, teamAdmin, th.BasicTeam) + member := th.AddUserToChannel(t, teamAdmin, channel) + // The admin tier has to come from the team role: this membership is a + // plain channel_user, so channel_admin cannot be the reason it passes. + require.False(t, member.SchemeAdmin) + client := loginNewClient(t, teamAdmin) + + f := createAdminField(t) + items := []model.PropertyValuePatchItem{ + {FieldID: f.ID, Value: json.RawMessage(`"team-admin-val"`)}, + } + values, resp, err := client.PatchPropertyValues(context.Background(), group.Name, "channel", channel.Id, items) + require.NoError(t, err) + CheckOKStatus(t, resp) + require.Len(t, values, 1) + require.Equal(t, json.RawMessage(`"team-admin-val"`), values[0].Value) + }) + + t.Run("team admin outside the channel is stopped by the outer property-write gate", func(t *testing.T) { + channel := newChannel(t, model.ChannelTypePrivate) + teamAdmin := th.CreateUser(t) + th.LinkUserToTeam(t, teamAdmin, th.BasicTeam) + th.UpdateUserToTeamAdmin(t, teamAdmin, th.BasicTeam) + client := loginNewClient(t, teamAdmin) + + f := createAdminField(t) + + // Inner tier is satisfied without membership: manage_channel_roles + // cascades from the team role. + require.True(t, th.App.HasPermissionToSetPropertyFieldValues(th.Context, teamAdmin.Id, f, channel.Id)) + + // The outer gate is independent and still rejects the write: + // manage_private_channel_properties is a channel_user grant that + // team_admin does not carry. + items := []model.PropertyValuePatchItem{ + {FieldID: f.ID, Value: json.RawMessage(`"outside-attempt"`)}, + } + _, resp, err := client.PatchPropertyValues(context.Background(), group.Name, "channel", channel.Id, items) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + }) + + t.Run("channel admin of one channel cannot write on another", func(t *testing.T) { + ownChannel := newChannel(t, model.ChannelTypeOpen) + otherChannel := newChannel(t, model.ChannelTypeOpen) + + channelAdmin := th.CreateUser(t) + th.LinkUserToTeam(t, channelAdmin, th.BasicTeam) + th.AddUserToChannel(t, channelAdmin, ownChannel) + th.MakeUserChannelAdmin(t, channelAdmin, ownChannel) + member := th.AddUserToChannel(t, channelAdmin, otherChannel) + require.False(t, member.SchemeAdmin) + client := loginNewClient(t, channelAdmin) + + f := createAdminField(t) + items := []model.PropertyValuePatchItem{ + {FieldID: f.ID, Value: json.RawMessage(`"cross-channel-attempt"`)}, + } + + // The tier is evaluated against the target channel, so channel_admin + // elsewhere grants nothing here. + _, resp, err := client.PatchPropertyValues(context.Background(), group.Name, "channel", otherChannel.Id, items) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + + values, resp, err := client.PatchPropertyValues(context.Background(), group.Name, "channel", ownChannel.Id, items) + require.NoError(t, err) + CheckOKStatus(t, resp) + require.Len(t, values, 1) + }) } func TestCreatePropertyFieldTeamScopedBroadcast(t *testing.T) { @@ -3904,6 +4210,194 @@ func TestCreatePropertyFieldTeamScopedBroadcast(t *testing.T) { }) } +// nextPropertyValuesEvent drains the websocket stream until the next +// property_values_updated event arrives, ignoring unrelated traffic. +func nextPropertyValuesEvent(t *testing.T, wsClient *model.WebSocketClient) *model.WebSocketEvent { + t.Helper() + + deadline := time.After(10 * time.Second) + for { + select { + case event, ok := <-wsClient.EventChannel: + require.True(t, ok, "websocket event channel closed while waiting for property_values_updated") + if event.EventType() == model.WebsocketEventPropertyValuesUpdated { + return event + } + case <-deadline: + require.FailNow(t, "timed out waiting for property_values_updated") + } + } +} + +func decodeBroadcastValues(t *testing.T, event *model.WebSocketEvent) []*model.PropertyValue { + t.Helper() + + raw, ok := event.GetData()["values"].(string) + require.True(t, ok, "event carries no values payload") + + var values []*model.PropertyValue + require.NoError(t, json.Unmarshal([]byte(raw), &values)) + return values +} + +// Locks the four payloads the server emits under the single +// property_values_updated event. The webapp tells an upsert from each deletion +// path purely by which keys are present, so a silent change to any shape breaks +// client cache invalidation without failing any other test: +// +// 1. upsert (incl. PATCH value:null) object_type + target_id + full values array +// 2. single delete object_type + target_id + one tombstone, ID empty +// 3. delete for target object_type + target_id + values "[]" +// 4. delete for field field_id only, no object_type/target_id, values "[]" +func TestPropertyValuesUpdatedPayloadShapes(t *testing.T) { + mainHelper.Parallel(t) + th := SetupConfig(t, func(cfg *model.Config) { + cfg.FeatureFlags.ChannelAttributes = true + }).InitBasic(t) + + group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{Name: "test_value_payload_shapes", Version: model.PropertyGroupVersionV2}) + require.Nil(t, appErr) + + memberLevel := model.PermissionLevelMember + newChannelField := func(t *testing.T) *model.PropertyField { + t.Helper() + field, fieldErr := th.App.CreatePropertyField(th.Context, &model.PropertyField{ + Name: model.NewId(), + Type: model.PropertyFieldTypeText, + GroupID: group.ID, + ObjectType: "channel", + TargetType: "system", + PermissionField: &memberLevel, + PermissionValues: &memberLevel, + PermissionOptions: &memberLevel, + }, false, "") + require.Nil(t, fieldErr) + return field + } + + th.LoginBasic(t) + webSocketClient := th.CreateConnectedWebSocketClient(t) + + field := newChannelField(t) + var valueID string + + t.Run("shape 1: upsert carries object_type, target_id, and the full values array", func(t *testing.T) { + items := []model.PropertyValuePatchItem{ + {FieldID: field.ID, Value: json.RawMessage(`"aurora"`)}, + } + upserted, resp, patchErr := th.Client.PatchPropertyValues(context.Background(), group.Name, "channel", th.BasicChannel.Id, items) + require.NoError(t, patchErr) + CheckOKStatus(t, resp) + require.Len(t, upserted, 1) + valueID = upserted[0].ID + + event := nextPropertyValuesEvent(t, webSocketClient) + require.Equal(t, "channel", event.GetData()["object_type"]) + require.Equal(t, th.BasicChannel.Id, event.GetData()["target_id"]) + require.Equal(t, th.BasicChannel.Id, event.GetBroadcast().ChannelId) + + values := decodeBroadcastValues(t, event) + require.Len(t, values, 1) + require.Equal(t, field.ID, values[0].FieldID) + require.Equal(t, th.BasicChannel.Id, values[0].TargetID) + require.Equal(t, json.RawMessage(`"aurora"`), values[0].Value) + }) + + t.Run("shape 1: PATCH value:null follows the upsert path and keeps a null-valued row", func(t *testing.T) { + // The user-initiated clear is not a deletion: the row survives holding + // null, so client selectors must treat null as unset rather than wait + // for a delete broadcast that never arrives. + items := []model.PropertyValuePatchItem{ + {FieldID: field.ID, Value: json.RawMessage(`null`)}, + } + _, resp, patchErr := th.Client.PatchPropertyValues(context.Background(), group.Name, "channel", th.BasicChannel.Id, items) + require.NoError(t, patchErr) + CheckOKStatus(t, resp) + + event := nextPropertyValuesEvent(t, webSocketClient) + require.Equal(t, "channel", event.GetData()["object_type"]) + require.Equal(t, th.BasicChannel.Id, event.GetData()["target_id"]) + + values := decodeBroadcastValues(t, event) + require.Len(t, values, 1) + require.Equal(t, field.ID, values[0].FieldID) + require.Equal(t, json.RawMessage(`null`), values[0].Value) + // Populated ID: this is a real row, which is what separates it from the + // delete tombstone below. + require.NotEmpty(t, values[0].ID) + }) + + t.Run("shape 2: single delete carries a one-element zero-valued tombstone", func(t *testing.T) { + require.NotEmpty(t, valueID) + require.Nil(t, th.App.DeletePropertyValue(th.Context, group.ID, valueID)) + + event := nextPropertyValuesEvent(t, webSocketClient) + require.Equal(t, "channel", event.GetData()["object_type"]) + require.Equal(t, th.BasicChannel.Id, event.GetData()["target_id"]) + + values := decodeBroadcastValues(t, event) + require.Len(t, values, 1) + require.Equal(t, field.ID, values[0].FieldID) + require.Equal(t, th.BasicChannel.Id, values[0].TargetID) + require.Equal(t, "channel", values[0].TargetType) + require.Equal(t, group.ID, values[0].GroupID) + + // The tombstone is synthesized without a Value, and a nil RawMessage + // marshals to null — so on the wire a delete is indistinguishable from + // the value:null clear above except for the empty ID. A client keying off + // the value alone leaves a stale row behind instead of removing it. + require.Equal(t, json.RawMessage(`null`), values[0].Value) + require.Empty(t, values[0].ID) + }) + + t.Run("shape 3: delete for target carries an empty values array", func(t *testing.T) { + targetField := newChannelField(t) + items := []model.PropertyValuePatchItem{ + {FieldID: targetField.ID, Value: json.RawMessage(`"noforn"`)}, + } + _, resp, patchErr := th.Client.PatchPropertyValues(context.Background(), group.Name, "channel", th.BasicChannel.Id, items) + require.NoError(t, patchErr) + CheckOKStatus(t, resp) + nextPropertyValuesEvent(t, webSocketClient) // the upsert from the line above + + require.Nil(t, th.App.DeletePropertyValuesForTarget(th.Context, group.ID, "channel", th.BasicChannel.Id)) + + event := nextPropertyValuesEvent(t, webSocketClient) + require.Equal(t, "channel", event.GetData()["object_type"]) + require.Equal(t, th.BasicChannel.Id, event.GetData()["target_id"]) + // Empty array, not tombstones: the event's target ID is the only thing + // identifying what to clear. + require.Equal(t, "[]", event.GetData()["values"]) + require.Empty(t, decodeBroadcastValues(t, event)) + }) + + t.Run("shape 4: delete for field carries field_id and no target", func(t *testing.T) { + fieldScoped := newChannelField(t) + items := []model.PropertyValuePatchItem{ + {FieldID: fieldScoped.ID, Value: json.RawMessage(`"elevated"`)}, + } + _, resp, patchErr := th.Client.PatchPropertyValues(context.Background(), group.Name, "channel", th.BasicChannel.Id, items) + require.NoError(t, patchErr) + CheckOKStatus(t, resp) + nextPropertyValuesEvent(t, webSocketClient) // the upsert from the line above + + require.Nil(t, th.App.DeletePropertyValuesForField(th.Context, group.ID, fieldScoped.ID)) + + event := nextPropertyValuesEvent(t, webSocketClient) + require.Equal(t, fieldScoped.ID, event.GetData()["field_id"]) + require.Equal(t, "[]", event.GetData()["values"]) + + // No target scoping: this shape clears one field across every target, so + // it broadcasts system-wide rather than to a channel. + _, hasObjectType := event.GetData()["object_type"] + require.False(t, hasObjectType, "delete-for-field must not carry object_type") + _, hasTargetID := event.GetData()["target_id"] + require.False(t, hasTargetID, "delete-for-field must not carry target_id") + require.Empty(t, event.GetBroadcast().ChannelId) + require.Empty(t, event.GetBroadcast().TeamId) + }) +} + func TestPatchPropertyValuesChannelObjectTypeBroadcast(t *testing.T) { mainHelper.Parallel(t) th := SetupConfig(t, func(cfg *model.Config) { diff --git a/server/channels/app/cpa_value_audit_test.go b/server/channels/app/cpa_value_audit_test.go new file mode 100644 index 000000000000..f5406e448473 --- /dev/null +++ b/server/channels/app/cpa_value_audit_test.go @@ -0,0 +1,151 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" + "github.com/mattermost/mattermost/server/v8/channels/app/properties" +) + +// The CPA value sink writes through AddMeta rather than event parameters, so +// auditParam does not apply here. +func auditMeta(t *testing.T, rec map[string]any, key string) any { + t.Helper() + meta, ok := rec[model.AuditKeyMeta].(map[string]any) + require.True(t, ok, "audit record has no meta") + return meta[key] +} + +func auditMetaHasKey(t *testing.T, rec map[string]any, key string) bool { + t.Helper() + meta, ok := rec[model.AuditKeyMeta].(map[string]any) + require.True(t, ok, "audit record has no meta") + _, present := meta[key] + return present +} + +// Pins the audit trail governance relies on to answer "who marked this channel, +// and as what". Asserts new_value is captured verbatim: masking hides some values +// from some administrators, so raw capture in the audit stream is a decision, and +// narrowing it must break this test rather than pass unnoticed. +func TestCPAValueChangeAuditForChannelValues(t *testing.T) { + th := Setup(t).InitBasic(t) + // LicenseCheckHook gates access_control writes on an Enterprise license. + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise)) + capture := startPluginAuditCapture(t, th) + + rctx := request.TestContext(t) + cpaGroup, groupErr := th.App.GetPropertyGroup(rctx, model.AccessControlPropertyGroupName) + require.Nil(t, groupErr) + + newChannelField := func(t *testing.T) *model.PropertyField { + t.Helper() + field, fieldErr := th.App.CreatePropertyField(rctx, &model.PropertyField{ + GroupID: cpaGroup.ID, + Name: "chan_attr_" + model.NewId(), + Type: model.PropertyFieldTypeText, + ObjectType: model.PropertyFieldObjectTypeChannel, + TargetType: string(model.PropertyFieldTargetLevelSystem), + }, false, "") + require.Nil(t, fieldErr) + return field + } + + writeValue := func(t *testing.T, field *model.PropertyField, raw string) *model.PropertyValue { + t.Helper() + values, upsertErr := th.App.UpsertPropertyValues(rctx, []*model.PropertyValue{{ + TargetID: th.BasicChannel.Id, + TargetType: model.PropertyFieldObjectTypeChannel, + GroupID: cpaGroup.ID, + FieldID: field.ID, + Value: json.RawMessage(raw), + }}, model.PropertyFieldObjectTypeChannel, th.BasicChannel.Id, "") + require.Nil(t, upsertErr) + require.Len(t, values, 1) + return values[0] + } + + nextRecord := func(t *testing.T, fn func()) map[string]any { + t.Helper() + before := len(capture.recordsFor(model.AuditEventCPAValueChange)) + fn() + records := capture.recordsFor(model.AuditEventCPAValueChange) + require.Len(t, records, before+1, "expected exactly one new %s record", model.AuditEventCPAValueChange) + return records[len(records)-1] + } + + t.Run("upsert records the channel target, the field, and the new value verbatim", func(t *testing.T) { + field := newChannelField(t) + + rec := nextRecord(t, func() { writeValue(t, field, `"AURORA"`) }) + + require.Equal(t, model.AuditStatusSuccess, rec[model.AuditKeyStatus]) + require.Equal(t, model.AccessControlPropertyGroupName, auditMeta(t, rec, "group")) + require.Equal(t, properties.ValueAuditActionUpsert, auditMeta(t, rec, "action")) + require.Equal(t, model.PropertyFieldObjectTypeChannel, auditMeta(t, rec, "target_type")) + require.Equal(t, th.BasicChannel.Id, auditMeta(t, rec, "target_id")) + require.Equal(t, field.ID, auditMeta(t, rec, "field_id")) + require.Equal(t, `"AURORA"`, auditMeta(t, rec, "new_value")) + }) + + t.Run("editing a value records the new value but not the previous one", func(t *testing.T) { + field := newChannelField(t) + writeValue(t, field, `"AURORA"`) + + rec := nextRecord(t, func() { writeValue(t, field, `"NOFORN"`) }) + + require.Equal(t, properties.ValueAuditActionUpsert, auditMeta(t, rec, "action")) + require.Equal(t, `"NOFORN"`, auditMeta(t, rec, "new_value")) + + // The sink supports prior_value but the upsert path never sets + // ValueAuditEvent.Prev, so an edit has no before/after pair. Asserted so + // the gap is visible rather than assumed absent. + require.False(t, auditMetaHasKey(t, rec, "prior_value")) + }) + + t.Run("deleting a single value records the prior value and the value id", func(t *testing.T) { + field := newChannelField(t) + value := writeValue(t, field, `"ELEVATED"`) + + rec := nextRecord(t, func() { + require.Nil(t, th.App.DeletePropertyValue(rctx, cpaGroup.ID, value.ID)) + }) + + require.Equal(t, properties.ValueAuditActionDelete, auditMeta(t, rec, "action")) + require.Equal(t, th.BasicChannel.Id, auditMeta(t, rec, "target_id")) + require.Equal(t, field.ID, auditMeta(t, rec, "field_id")) + require.Equal(t, value.ID, auditMeta(t, rec, "value_id")) + require.Equal(t, `"ELEVATED"`, auditMeta(t, rec, "prior_value")) + }) + + t.Run("bulk deletes record the scope but carry no value payload", func(t *testing.T) { + targetField := newChannelField(t) + writeValue(t, targetField, `"UNCLASSIFIED"`) + + rec := nextRecord(t, func() { + require.Nil(t, th.App.DeletePropertyValuesForTarget(rctx, cpaGroup.ID, model.PropertyFieldObjectTypeChannel, th.BasicChannel.Id)) + }) + require.Equal(t, properties.ValueAuditActionDeleteForTarget, auditMeta(t, rec, "action")) + require.Equal(t, th.BasicChannel.Id, auditMeta(t, rec, "target_id")) + require.False(t, auditMetaHasKey(t, rec, "prior_value")) + require.False(t, auditMetaHasKey(t, rec, "new_value")) + + fieldScoped := newChannelField(t) + writeValue(t, fieldScoped, `"SECRET"`) + + rec = nextRecord(t, func() { + require.Nil(t, th.App.DeletePropertyValuesForField(rctx, cpaGroup.ID, fieldScoped.ID)) + }) + require.Equal(t, properties.ValueAuditActionDeleteForField, auditMeta(t, rec, "action")) + require.Equal(t, fieldScoped.ID, auditMeta(t, rec, "field_id")) + require.False(t, auditMetaHasKey(t, rec, "prior_value")) + require.False(t, auditMetaHasKey(t, rec, "new_value")) + }) +} diff --git a/server/channels/app/properties/access_control_attribute_validation.go b/server/channels/app/properties/access_control_attribute_validation.go index ea6d69afb984..bdf1775da869 100644 --- a/server/channels/app/properties/access_control_attribute_validation.go +++ b/server/channels/app/properties/access_control_attribute_validation.go @@ -41,6 +41,11 @@ type PermissionChecker func(userID string, permission *model.Permission) bool // ldap/saml on non-text fields) // - auto-assigns IDs to options that lack one and validates option shape // - validates visibility, value_type, managed, display_name, and sort_order +// - validates and canonicalizes actions, the render-placement allow-list +// shared by the classification banner and channel labels; on update this +// fires only when actions actually change, so a field carrying a value +// that is no longer valid stays editable on all other attrs (lenient +// grandfather, as for Name) // - validates property values for text fields against value_type // constraints (email, url, phone) // - enforces that managed="admin" can only be set by callers with @@ -80,8 +85,9 @@ func (h *AccessControlAttributeValidationHook) isGroupManaged(groupID string) bo // default, clears attrs that don't apply to the field type, validates each // attr, and auto-IDs+validates options for select-shaped fields. Mutates // field.Attrs in place. prevType is the field's type before this operation. -// prevType is empty on creation of a new field. -func (h *AccessControlAttributeValidationHook) sanitizeAndValidateFieldAttrs(field *model.PropertyField, prevType model.PropertyFieldType) error { +// prevType is empty on creation of a new field. prevActions is the field's +// attrs["actions"] before this operation, nil on creation. +func (h *AccessControlAttributeValidationHook) sanitizeAndValidateFieldAttrs(field *model.PropertyField, prevType model.PropertyFieldType, prevActions any) error { if field.Attrs == nil { field.Attrs = model.StringInterface{} } @@ -136,9 +142,48 @@ func (h *AccessControlAttributeValidationHook) sanitizeAndValidateFieldAttrs(fie if err := model.ValidatePropertyFieldSortOrder(field); err != nil { return fmt.Errorf("%s: %w", err.Error(), ErrInvalidFieldAttrs) } + // Lenient grandfather, same rationale as Name: a PATCH merges attrs, so a + // field carrying an actions value that predates (or has since fallen out of) + // the allow-list would otherwise be unpatchable on every other attr, with + // delete/recreate the only way out. Only a caller actually changing actions + // gets the strict check; an untouched value rides along as-is. + if err := model.SanitizeAndValidatePropertyFieldActions(field); err != nil { + if !sameFieldActions(field.Attrs[model.PropertyFieldAttrActions], prevActions) { + return fmt.Errorf("%s: %w", err.Error(), ErrInvalidFieldAttrs) + } + } return nil } +// sameFieldActions reports whether two raw attrs["actions"] values carry the +// same list. Anything that isn't a list of strings compares unequal, so a +// caller sending a malformed value is still rejected rather than grandfathered. +func sameFieldActions(a, b any) bool { + as, aOK := fieldActionsAsStrings(a) + bs, bOK := fieldActionsAsStrings(b) + return aOK && bOK && slices.Equal(as, bs) +} + +func fieldActionsAsStrings(raw any) ([]string, bool) { + switch v := raw.(type) { + case nil: + return nil, true + case []string: + return v, true + case []any: + out := make([]string, 0, len(v)) + for _, elem := range v { + s, ok := elem.(string) + if !ok { + return nil, false + } + out = append(out, s) + } + return out, true + } + return nil, false +} + // trimmedFieldAttrKeys lists the string-valued attrs the hook trims on the // way in. Listed explicitly rather than iterating Attrs to avoid touching // keys this hook doesn't own (e.g. plugin-set attrs). @@ -458,9 +503,10 @@ func (h *AccessControlAttributeValidationHook) PreCreatePropertyField(rctx reque return nil, appErr } - // Create: no prior type, so a rank field here is authored directly and its - // ranks are validated strictly rather than repaired. - if err := h.sanitizeAndValidateFieldAttrs(field, ""); err != nil { + // Create: no prior type or actions, so a rank field here is authored directly + // and its ranks are validated strictly rather than repaired, and actions get + // the strict check with nothing to grandfather. + if err := h.sanitizeAndValidateFieldAttrs(field, "", nil); err != nil { return nil, err } @@ -485,7 +531,7 @@ func (h *AccessControlAttributeValidationHook) PreUpdatePropertyField(rctx reque } } - if err := h.sanitizeAndValidateFieldAttrs(field, existing.Type); err != nil { + if err := h.sanitizeAndValidateFieldAttrs(field, existing.Type, existing.Attrs[model.PropertyFieldAttrActions]); err != nil { return nil, err } @@ -524,10 +570,12 @@ func (h *AccessControlAttributeValidationHook) PreUpdatePropertyFields(rctx requ // the not-found error later); strict rank validation is the safe // default for that path. var prevType model.PropertyFieldType + var prevActions any if existing != nil { prevType = existing.Type + prevActions = existing.Attrs[model.PropertyFieldAttrActions] } - if err := h.sanitizeAndValidateFieldAttrs(field, prevType); err != nil { + if err := h.sanitizeAndValidateFieldAttrs(field, prevType, prevActions); err != nil { return nil, fmt.Errorf("field %s: %w", field.ID, err) } diff --git a/server/channels/app/properties/access_control_attribute_validation_test.go b/server/channels/app/properties/access_control_attribute_validation_test.go index 695f7c0f897f..b492a4bbc387 100644 --- a/server/channels/app/properties/access_control_attribute_validation_test.go +++ b/server/channels/app/properties/access_control_attribute_validation_test.go @@ -80,6 +80,72 @@ func TestAccessControlAttributeValidationHook(t *testing.T) { assert.NotEmpty(t, created.ID) }) + t.Run("allows classification and smart label actions on create", func(t *testing.T) { + field := &model.PropertyField{ + GroupID: group.ID, + Name: "field_" + model.NewId(), + Type: model.PropertyFieldTypeText, + TargetType: "system", + ObjectType: "channel", + Attrs: model.StringInterface{model.PropertyFieldAttrActions: []any{ + model.PropertyFieldActionDisplayBannerTop, + model.PropertyFieldActionDisplayLabelHeader, + model.PropertyFieldActionDisplayLabelInfo, + }}, + } + created, createErr := th.service.CreatePropertyField(th.Context, field) + require.NoError(t, createErr) + require.Equal(t, []any{ + model.PropertyFieldActionDisplayBannerTop, + model.PropertyFieldActionDisplayLabelHeader, + model.PropertyFieldActionDisplayLabelInfo, + }, created.Attrs[model.PropertyFieldAttrActions]) + }) + + // The classification banner is the only production writer of actions today, + // on a system-object linked field. Its exact round trip has to keep working: + // this validation is new, the banner is not. + t.Run("classification banner actions round-trip through create, patch, and clear", func(t *testing.T) { + field, createErr := th.service.CreatePropertyField(th.Context, &model.PropertyField{ + GroupID: group.ID, + Name: "field_" + model.NewId(), + Type: model.PropertyFieldTypeText, + TargetType: "system", + ObjectType: "system", + Attrs: model.StringInterface{model.PropertyFieldAttrActions: []any{ + model.PropertyFieldActionDisplayBannerTop, + model.PropertyFieldActionDisplayBannerBottom, + }}, + }) + require.NoError(t, createErr) + + field.Attrs = model.StringInterface{model.PropertyFieldAttrActions: []any{model.PropertyFieldActionDisplayBannerTop}} + updated, _, updateErr := th.service.UpdatePropertyField(th.Context, group.ID, field) + require.NoError(t, updateErr) + require.Equal(t, []any{model.PropertyFieldActionDisplayBannerTop}, updated.Attrs[model.PropertyFieldAttrActions]) + + // Disabling the banner sends an empty list, which clears the key rather + // than storing []. Both read as "no actions" client-side. + updated.Attrs = model.StringInterface{model.PropertyFieldAttrActions: []any{}} + cleared, _, clearErr := th.service.UpdatePropertyField(th.Context, group.ID, updated) + require.NoError(t, clearErr) + require.NotContains(t, cleared.Attrs, model.PropertyFieldAttrActions) + }) + + t.Run("rejects invalid actions on create", func(t *testing.T) { + field := &model.PropertyField{ + GroupID: group.ID, + Name: "field_" + model.NewId(), + Type: model.PropertyFieldTypeText, + TargetType: "system", + ObjectType: "channel", + Attrs: model.StringInterface{model.PropertyFieldAttrActions: []any{"unknown"}}, + } + _, createErr := th.service.CreatePropertyField(th.Context, field) + require.Error(t, createErr) + assert.Contains(t, createErr.Error(), "unknown action") + }) + t.Run("rejects invalid visibility on update", func(t *testing.T) { field := th.CreatePropertyFieldDirect(t, &model.PropertyField{ GroupID: group.ID, @@ -95,6 +161,49 @@ func TestAccessControlAttributeValidationHook(t *testing.T) { assert.Contains(t, updateErr.Error(), "visibility") }) + t.Run("rejects invalid actions on update", func(t *testing.T) { + field := th.CreatePropertyFieldDirect(t, &model.PropertyField{ + GroupID: group.ID, + Name: "field_" + model.NewId(), + Type: model.PropertyFieldTypeText, + TargetType: "system", + ObjectType: "channel", + }) + + field.Attrs = model.StringInterface{model.PropertyFieldAttrActions: []any{"unknown"}} + _, _, updateErr := th.service.UpdatePropertyField(th.Context, group.ID, field) + require.Error(t, updateErr) + assert.Contains(t, updateErr.Error(), "unknown action") + }) + + // Mirrors the lenient grandfather for Name: attrs are merged on PATCH, so a + // field carrying an actions value that is no longer valid has to stay editable + // on every other attr rather than needing delete/recreate to fix. + t.Run("grandfathers an untouched invalid actions value on update", func(t *testing.T) { + field := th.CreatePropertyFieldDirect(t, &model.PropertyField{ + GroupID: group.ID, + Name: "field_" + model.NewId(), + Type: model.PropertyFieldTypeText, + TargetType: "system", + ObjectType: "channel", + Attrs: model.StringInterface{model.PropertyFieldAttrActions: []any{"retired_action"}}, + }) + + field.Attrs = model.StringInterface{ + model.PropertyFieldAttrActions: []any{"retired_action"}, + model.PropertyFieldAttrDisplayName: "Renamed", + } + updated, _, updateErr := th.service.UpdatePropertyField(th.Context, group.ID, field) + require.NoError(t, updateErr) + assert.Equal(t, "Renamed", updated.Attrs[model.PropertyFieldAttrDisplayName]) + + // Changing actions still gets the strict check. + field.Attrs = model.StringInterface{model.PropertyFieldAttrActions: []any{"retired_action", "unknown"}} + _, _, updateErr = th.service.UpdatePropertyField(th.Context, group.ID, field) + require.Error(t, updateErr) + assert.Contains(t, updateErr.Error(), "unknown action") + }) + t.Run("skips validation for unmanaged groups", func(t *testing.T) { otherGroup, groupErr := th.service.RegisterPropertyGroup(&model.PropertyGroup{Name: "test_other_group", Version: model.PropertyGroupVersionV2}) require.NoError(t, groupErr) diff --git a/server/go.mod b/server/go.mod index cfd446584f44..dd31340b2848 100644 --- a/server/go.mod +++ b/server/go.mod @@ -50,7 +50,7 @@ require ( github.com/mattermost/mattermost-plugin-ai v1.14.2 github.com/mattermost/mattermost/server/public v0.4.3 github.com/mattermost/morph v1.1.0 - github.com/mattermost/pdf v0.0.0-20260728101013-cd8a834041c4 + github.com/mattermost/pdf v0.0.0-20260828123129-5b7509a6ca01 github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 github.com/mattermost/squirrel v0.5.0 github.com/mholt/archives v0.1.5 diff --git a/server/go.sum b/server/go.sum index 795245356754..8d9cbc5b0910 100644 --- a/server/go.sum +++ b/server/go.sum @@ -373,8 +373,8 @@ github.com/mattermost/morph v1.1.0 h1:Q9vrJbeM3s2jfweGheq12EFIzdNp9a/6IovcbvOQ6C github.com/mattermost/morph v1.1.0/go.mod h1:gD+EaqX2UMyyuzmF4PFh4r33XneQ8Nzi+0E8nXjMa3A= github.com/mattermost/msgpack/v5 v5.0.0-20260408165622-cadfad56a815 h1:uOi89NvrFmDngqMKjlLDxi+MNzJQLA3TqcU2p8czv34= github.com/mattermost/msgpack/v5 v5.0.0-20260408165622-cadfad56a815/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/mattermost/pdf v0.0.0-20260728101013-cd8a834041c4 h1:iFMZLV6k/gOXuQmETk+Ym7FGn5nrBAKlQ0kY/FgK+ro= -github.com/mattermost/pdf v0.0.0-20260728101013-cd8a834041c4/go.mod h1:pNks5J7leEpCnswIJaGfqiz/MQ0lExHgKl+A53aEXrg= +github.com/mattermost/pdf v0.0.0-20260828123129-5b7509a6ca01 h1:bXDcd5MREhXeaY+Gn1qixqN91GEClojJBG9v98cbB6s= +github.com/mattermost/pdf v0.0.0-20260828123129-5b7509a6ca01/go.mod h1:pNks5J7leEpCnswIJaGfqiz/MQ0lExHgKl+A53aEXrg= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 h1:G9tL6JXRBMzjuD1kkBtcnd42kUiT6QDwxfFYu7adM6o= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= github.com/mattermost/squirrel v0.5.0 h1:81QPS0aA+inQbpA7Pzmv6O9sWwB6VaBh/VYw3oJf8ZY= diff --git a/server/public/model/feature_flags.go b/server/public/model/feature_flags.go index 9da0b195a8ac..1d6511e79a39 100644 --- a/server/public/model/feature_flags.go +++ b/server/public/model/feature_flags.go @@ -133,6 +133,9 @@ type FeatureFlags struct { TeamMembershipAccessControl bool + // Enable channel attributes (Smart Labels, banners) powered by the Properties API. + ChannelAttributes bool + // FEATURE_FLAG_REMOVAL: ResourceAttributesInPolicies - Remove this when the // feature is GA. Gates access rules that compare a user's attributes against // the accessed channel's (resource.attributes.*): when off, the autocomplete @@ -220,6 +223,8 @@ func (f *FeatureFlags) SetDefaults() { f.PropertyFieldRank = true + f.ChannelAttributes = false + f.MmBlocksEnabled = true f.ChannelBookmarks = true diff --git a/server/public/model/feature_flags_test.go b/server/public/model/feature_flags_test.go index 4c159340aff8..87e220a7fe94 100644 --- a/server/public/model/feature_flags_test.go +++ b/server/public/model/feature_flags_test.go @@ -26,6 +26,14 @@ func TestFeatureFlagsSetDefaults(t *testing.T) { require.Equal(t, "false", m["ClassificationMarkings"]) }) + t.Run("ChannelAttributes should default to false and serialize correctly", func(t *testing.T) { + require.False(t, f.ChannelAttributes) + require.Equal(t, "false", f.ToMap()["ChannelAttributes"]) + + f.ChannelAttributes = true + require.Equal(t, "true", f.ToMap()["ChannelAttributes"]) + }) + t.Run("MmBlocksEnabled defaults to true", func(t *testing.T) { require.True(t, f.MmBlocksEnabled) require.Equal(t, "true", f.ToMap()["MmBlocksEnabled"]) diff --git a/server/public/model/property_field_attrs_validation.go b/server/public/model/property_field_attrs_validation.go index 2e2924455c7d..5b96e423fe29 100644 --- a/server/public/model/property_field_attrs_validation.go +++ b/server/public/model/property_field_attrs_validation.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "net/url" + "slices" "strings" ) @@ -20,6 +21,16 @@ const ( PropertyFieldAttrSAML = "saml" PropertyFieldAttrManaged = "managed" PropertyFieldAttrDisplayName = "display_name" + // PropertyFieldAttrActions lists the rendering actions a field triggers. + PropertyFieldAttrActions = "actions" +) + +// Valid action values for PropertyFieldAttrActions. +const ( + PropertyFieldActionDisplayBannerTop = "display_banner_top" + PropertyFieldActionDisplayBannerBottom = "display_banner_bottom" + PropertyFieldActionDisplayLabelHeader = "display_label_header" + PropertyFieldActionDisplayLabelInfo = "display_label_info" ) // Valid visibility values for property fields. @@ -63,6 +74,20 @@ func IsValidPropertyFieldValueType(v string) bool { } } +// validPropertyFieldActions is the allow-list for PropertyFieldAttrActions, and +// the source of the list surfaced in validation errors. +var validPropertyFieldActions = []string{ + PropertyFieldActionDisplayBannerTop, + PropertyFieldActionDisplayBannerBottom, + PropertyFieldActionDisplayLabelHeader, + PropertyFieldActionDisplayLabelInfo, +} + +// IsValidPropertyFieldAction reports whether the given string is a known action value. +func IsValidPropertyFieldAction(a string) bool { + return slices.Contains(validPropertyFieldActions, a) +} + // ValidatePropertyFieldVisibility checks that the visibility attr on a // PropertyField is either empty or one of hidden/when_set/always. func ValidatePropertyFieldVisibility(field *PropertyField) error { @@ -112,6 +137,69 @@ func ValidatePropertyFieldSortOrder(field *PropertyField) error { } } +// SanitizeAndValidatePropertyFieldActions validates the actions attr and writes +// it back in the canonical []any form of trimmed strings, so downstream readers +// see a single shape. An absent, nil, or empty list is removed. Unknown actions +// and duplicates are rejected: the values drive rendering, so a typo must fail +// loudly at write time rather than silently never render. +func SanitizeAndValidatePropertyFieldActions(field *PropertyField) error { + if field.Attrs == nil { + return nil + } + + raw, ok := field.Attrs[PropertyFieldAttrActions] + if !ok { + return nil + } + if raw == nil { + delete(field.Attrs, PropertyFieldAttrActions) + return nil + } + + // Callers reach this both from JSON ([]any) and from Go code ([]string). + var items []string + switch v := raw.(type) { + case []string: + items = v + case []any: + items = make([]string, 0, len(v)) + for i, elem := range v { + s, ok := elem.(string) + if !ok { + return fmt.Errorf("actions[%d] must be a string, got %T", i, elem) + } + items = append(items, s) + } + default: + return fmt.Errorf("actions must be an array, got %T", raw) + } + + if len(items) == 0 { + delete(field.Attrs, PropertyFieldAttrActions) + return nil + } + + seen := make(map[string]struct{}, len(items)) + canonical := make([]any, 0, len(items)) + for _, s := range items { + s = strings.TrimSpace(s) + if s == "" { + return fmt.Errorf("actions must not contain empty strings") + } + if !IsValidPropertyFieldAction(s) { + return fmt.Errorf("unknown action %q: must be one of %s", s, strings.Join(validPropertyFieldActions, ", ")) + } + if _, dup := seen[s]; dup { + return fmt.Errorf("duplicate action %q", s) + } + seen[s] = struct{}{} + canonical = append(canonical, s) + } + + field.Attrs[PropertyFieldAttrActions] = canonical + return nil +} + // ValidatePropertyValueForValueType validates a raw JSON value against the // given value type constraint. This is called for text fields that have a // value_type attr (email, url, phone). diff --git a/server/public/model/property_field_attrs_validation_test.go b/server/public/model/property_field_attrs_validation_test.go index a90f4902fb7b..c762dd3d0c86 100644 --- a/server/public/model/property_field_attrs_validation_test.go +++ b/server/public/model/property_field_attrs_validation_test.go @@ -69,6 +69,64 @@ func TestValidatePropertyFieldSortOrder(t *testing.T) { } } +func TestSanitizeAndValidatePropertyFieldActions(t *testing.T) { + tests := []struct { + name string + attrs StringInterface + want []any + wantKey bool + wantError string + }{ + {name: "nil attrs", attrs: nil}, + {name: "no actions key", attrs: StringInterface{"other": "value"}}, + {name: "nil actions", attrs: StringInterface{PropertyFieldAttrActions: nil}}, + {name: "empty typed actions", attrs: StringInterface{PropertyFieldAttrActions: []string{}}}, + {name: "empty decoded actions", attrs: StringInterface{PropertyFieldAttrActions: []any{}}}, + { + name: "classification banner actions", + attrs: StringInterface{PropertyFieldAttrActions: []any{PropertyFieldActionDisplayBannerTop, PropertyFieldActionDisplayBannerBottom}}, + want: []any{PropertyFieldActionDisplayBannerTop, PropertyFieldActionDisplayBannerBottom}, + wantKey: true, + }, + { + name: "smart label actions are trimmed and canonicalized", + attrs: StringInterface{PropertyFieldAttrActions: []string{" " + PropertyFieldActionDisplayLabelHeader + " ", PropertyFieldActionDisplayLabelInfo}}, + want: []any{PropertyFieldActionDisplayLabelHeader, PropertyFieldActionDisplayLabelInfo}, + wantKey: true, + }, + {name: "non-array", attrs: StringInterface{PropertyFieldAttrActions: "display_label_header"}, wantError: "actions must be an array"}, + {name: "non-string element", attrs: StringInterface{PropertyFieldAttrActions: []any{42}}, wantError: "actions[0] must be a string"}, + {name: "empty action", attrs: StringInterface{PropertyFieldAttrActions: []any{" "}}, wantError: "actions must not contain empty strings"}, + {name: "unknown action", attrs: StringInterface{PropertyFieldAttrActions: []any{"unknown"}}, wantError: "unknown action"}, + { + name: "duplicate action", + attrs: StringInterface{PropertyFieldAttrActions: []any{PropertyFieldActionDisplayLabelHeader, PropertyFieldActionDisplayLabelHeader}}, + wantError: "duplicate action", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + field := &PropertyField{Attrs: tt.attrs} + err := SanitizeAndValidatePropertyFieldActions(field) + if tt.wantError != "" { + require.ErrorContains(t, err, tt.wantError) + return + } + + require.NoError(t, err) + if field.Attrs == nil { + return + } + actual, ok := field.Attrs[PropertyFieldAttrActions] + require.Equal(t, tt.wantKey, ok) + if tt.wantKey && tt.want != nil { + require.Equal(t, tt.want, actual) + } + }) + } +} + func TestValidatePropertyValueForValueType(t *testing.T) { tests := []struct { name string diff --git a/webapp/platform/types/src/config.ts b/webapp/platform/types/src/config.ts index 69a9f5810c95..cac638e16e24 100644 --- a/webapp/platform/types/src/config.ts +++ b/webapp/platform/types/src/config.ts @@ -132,6 +132,7 @@ export type ClientConfig = { FeatureFlagPolicySimulation: string; FeatureFlagContentFlagging: string; FeatureFlagClassificationMarkings: string; + FeatureFlagChannelAttributes: string; FeatureFlagPropertyFieldRank: string; FeatureFlagManagedChannelCategories: string; FeatureFlagSessionAttributes: string;