Skip to content

Upgrade plugin to support Mattermost 11.8#3

Open
davidkrauser wants to merge 1 commit into
mainfrom
upgrade-mattermost-11.8
Open

Upgrade plugin to support Mattermost 11.8#3
davidkrauser wants to merge 1 commit into
mainfrom
upgrade-mattermost-11.8

Conversation

@davidkrauser
Copy link
Copy Markdown
Collaborator

@davidkrauser davidkrauser commented May 27, 2026

Summary

Mattermost 11.8 reworked how user attribute fields are stored. The property group they live in was renamed from custom_profile_attributes to access_control, the data model changed shape, and the server now enforces validation rules that didn't exist before. The plugin failed to activate against 11.8 because of these changes:

"custom_profile_attributes" is a version 1 PSA group that has been deprecated;
use the version 2 PSA group "access_control" instead

This PR brings the plugin up to date. It's intended as a reference for other plugin authors who store user attributes and need to make the same upgrade.

What needed to change

  • Bumped the server/public dependency to v0.4.1, which is the first release containing the new system. Mattermost server's own go.mod pins Go 1.26.3, so the plugin's minimum Go version moved up to match. The minimum Mattermost server version went from 11.5.0 to 11.8.0.

  • Looked up the property group by its new name. The old name custom_profile_attributes is rejected outright now. The new name is exposed as model.AccessControlPropertyGroupName.

  • Set ObjectType and TargetType on every field. The new system requires fields to declare what kind of object they describe (user) and at what scope they apply (system, meaning the field is defined once globally and applies to every user). The old system didn't require either. We also explicitly set ObjectType: user rather than relying on a default, both because the system rejects fields without it and because this plugin only ever creates user fields.

  • Renamed field identifiers to snake_case and added a separate display name. The server now requires field names to be valid identifiers (letters, digits, underscores, no spaces) because those names are referenced from ABAC policy expressions. Display names — the human-readable labels you see in the UI — used to share the same field but now live in a separate display_name attribute. So "Job Title" became Name: "job_title" with DisplayName: "Job Title". Our fieldDefinition struct was already close to this shape; the rename made it match.

  • Pinned all three permission levels to sysadmin. New fields now have three permission settings (PermissionField, PermissionValues, PermissionOptions) that say who, role-wise, is allowed to change different aspects of the field. For protected fields like ours, two of these don't really do anything — the server auto-pins them. But the default for PermissionValues is "members can edit their own value," and the server rejects that combined with shared_only access (the two are contradictory — if anyone could write any value, they could fake having values in common with anyone). We override PermissionValues to sysadmin to clear that validation, and set the other two explicitly for clarity. See the comment in createField for the full explanation.

  • Switched to the canonical attribute constants. The old model.CustomProfileAttributesPropertyAttrs* constants are now aliases for the canonical model.PropertyFieldAttr* constants. Swapped to the canonical names since we're no longer thinking of these as a CPA-only concern.

🤖 Generated with Claude Code

Mattermost 11.8 moved user attribute fields out of the deprecated
custom_profile_attributes property group and into the access_control
group, with a new data shape and stricter validation. This commit
updates the plugin to work against the new system.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@davidkrauser davidkrauser requested a review from isacikgoz May 27, 2026 21:43
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request migrates the Mattermost user attribute sync plugin from the deprecated Custom Profile Attributes property group to the access_control property group, enabling attributes to be addressable in ABAC policy rules via user.attributes.<field_name>. The change includes Go dependency updates, plugin structure refactoring, field definition schema redesign with snake_case naming, and corresponding test updates.

Changes

User attribute sync migration from CPA to access_control

Layer / File(s) Summary
Documentation and metadata updates
AGENTS.md, README.md, plugin.json, server/sync/provider.go, server/sync/value_sync.go, server/job.go
All documentation, README intro, plugin metadata, and inline comments updated to describe synchronization into access_control property group and ABAC availability via user.attributes.<field_name> instead of CPA terminology; minimum Mattermost version raised to 11.8.0 and Go version to 1.26.3.
Go dependencies upgrade
go.mod
Go toolchain updated to 1.26.3, mattermost/server/public direct dependency bumped to v0.4.1, and numerous indirect dependencies refreshed including grpc, protobuf, and genproto versions.
Plugin struct migration and property group resolution
server/plugin.go
Plugin type replaces cpaGroupID with groupID field and imports AccessControlPropertyGroupName constant; OnActivate looks up the access_control property group ID and stores it for field sync operations.
Job sync execution with new group context
server/job.go
runSync method passes p.groupID to sync.SyncUsers instead of p.cpaGroupID, using the access_control property group established during plugin activation.
Field definition schema refactoring
server/sync/field_sync.go
fieldDefinition struct removes ExternalName and uses canonical Name/DisplayName/Type/OptionNames; fieldDefinitions data updated to snake_case field names (job_title, programs, start_date); createField and updateField logic now set sysadmin permissions, user ObjectType, system TargetType, and new attribute keys; syncSingleField cache key uses def.Name.
Field sync test updates
server/sync/field_sync_test.go
Test fixtures and mocks updated to use snake_case field names; conditional logic for multiselect field handling branches on f.Name == "programs"; cache assertions expect snake_case keyed field IDs.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: upgrading the plugin to support the new Mattermost 11.8 version, which aligns with all file modifications including dependency bumps, property group migration, and API updates.
Description check ✅ Passed The description is highly detailed and directly related to the changeset. It explains the motivation for the upgrade, specifically details each required change to support Mattermost 11.8's reworked user attribute storage system, and provides technical context about property group renaming, data model changes, and validation requirements.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch upgrade-mattermost-11.8

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
AGENTS.md (1)

15-23: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language hint to the architecture fenced block.

The fenced block is missing a language identifier, which triggers markdownlint MD040.

Suggested fix
-```
+```text
 Plugin Activation (Once)
   ├─> Create/Update User Attribute Fields (schema)
   └─> Start Background Job (cluster-aware)

 Background Job (Configurable interval, default 60min)
   ├─> Fetch Changed Values From Provider
   └─> Bulk Upsert Values via PropertyService
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @AGENTS.md around lines 15 - 23, The markdown fenced block in AGENTS.md lacks
a language identifier (triggering markdownlint MD040); update the opening
triple-backtick for the architecture block to include a language hint (e.g.,
replace withtext) so the block is explicitly marked as plain text and
the linter warning is resolved.


</details>

</blockquote></details>
<details>
<summary>README.md (1)</summary><blockquote>

`17-25`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Specify a language for the architecture fenced block.**

This block is missing a fence language and triggers markdownlint MD040.

 
<details>
<summary>Suggested fix</summary>

```diff
-```
+```text
 Plugin Activation (Once)
   ├─> Create/Update User Attribute Fields
   └─> Start Background Job

 Background Job (On timed interval)
   ├─> Fetch Changed Values From External Source
   └─> Bulk Upsert Values
 ```
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @README.md around lines 17 - 25, The fenced code block showing the
architecture diagram is missing a language specifier, causing markdownlint
MD040; update the triple-backtick fence that encloses the "Plugin Activation
(Once) ..." block to include a language (e.g., "text") so it becomes ```text to
satisfy the linter and preserve formatting; locate the fenced block in README.md
and edit the opening fence only.


</details>

</blockquote></details>

</blockquote></details>
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/sync/field_sync.go`:
- Around line 108-116: The update block that modifies existingField (inside
updateField) is missing an update for the access mode; add a line to set
existingField.Attrs[model.PropertyAttrsAccessMode] from the incoming field
definition (e.g. existingField.Attrs[model.PropertyAttrsAccessMode] =
def.Attrs[model.PropertyAttrsAccessMode] or, if the definition exposes
AccessMode directly, existingField.Attrs[model.PropertyAttrsAccessMode] =
def.AccessMode) so that access mode changes applied in createField are also
applied when updating existing fields.

---

Outside diff comments:
In `@AGENTS.md`:
- Around line 15-23: The markdown fenced block in AGENTS.md lacks a language
identifier (triggering markdownlint MD040); update the opening triple-backtick
for the architecture block to include a language hint (e.g., replace ``` with
```text) so the block is explicitly marked as plain text and the linter warning
is resolved.

In `@README.md`:
- Around line 17-25: The fenced code block showing the architecture diagram is
missing a language specifier, causing markdownlint MD040; update the
triple-backtick fence that encloses the "Plugin Activation (Once) ..." block to
include a language (e.g., "text") so it becomes ```text to satisfy the linter
and preserve formatting; locate the fenced block in README.md and edit the
opening fence only.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b5a5dd8-87dc-45a2-b8a8-98aea1ffccd1

📥 Commits

Reviewing files that changed from the base of the PR and between 4cfb821 and cb9af4e.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (10)
  • AGENTS.md
  • README.md
  • go.mod
  • plugin.json
  • server/job.go
  • server/plugin.go
  • server/sync/field_sync.go
  • server/sync/field_sync_test.go
  • server/sync/provider.go
  • server/sync/value_sync.go

Comment thread server/sync/field_sync.go
Comment on lines 108 to +116
existingField.Type = def.Type
existingField.Attrs[model.CustomProfileAttributesPropertyAttrsVisibility] = model.CustomProfileAttributesVisibilityAlways
existingField.Attrs[model.PropertyFieldAttrVisibility] = model.PropertyFieldVisibilityAlways
existingField.Attrs[model.PropertyFieldAttrDisplayName] = def.DisplayName
existingField.Attrs[model.PropertyAttrsProtected] = true
// See createField for why all three permission levels are set to sysadmin.
sysadmin := model.PermissionLevelSysadmin
existingField.PermissionField = &sysadmin
existingField.PermissionValues = &sysadmin
existingField.PermissionOptions = &sysadmin
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing AccessMode update for existing fields.

createField sets model.PropertyAttrsAccessMode (line 215), but updateField does not. If a field definition's AccessMode changes between plugin versions, existing fields will retain the old value.

🔧 Proposed fix
 	existingField.Attrs[model.PropertyFieldAttrVisibility] = model.PropertyFieldVisibilityAlways
 	existingField.Attrs[model.PropertyFieldAttrDisplayName] = def.DisplayName
 	existingField.Attrs[model.PropertyAttrsProtected] = true
+	existingField.Attrs[model.PropertyAttrsAccessMode] = def.AccessMode
 	// See createField for why all three permission levels are set to sysadmin.
 	sysadmin := model.PermissionLevelSysadmin
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
existingField.Type = def.Type
existingField.Attrs[model.CustomProfileAttributesPropertyAttrsVisibility] = model.CustomProfileAttributesVisibilityAlways
existingField.Attrs[model.PropertyFieldAttrVisibility] = model.PropertyFieldVisibilityAlways
existingField.Attrs[model.PropertyFieldAttrDisplayName] = def.DisplayName
existingField.Attrs[model.PropertyAttrsProtected] = true
// See createField for why all three permission levels are set to sysadmin.
sysadmin := model.PermissionLevelSysadmin
existingField.PermissionField = &sysadmin
existingField.PermissionValues = &sysadmin
existingField.PermissionOptions = &sysadmin
existingField.Type = def.Type
existingField.Attrs[model.PropertyFieldAttrVisibility] = model.PropertyFieldVisibilityAlways
existingField.Attrs[model.PropertyFieldAttrDisplayName] = def.DisplayName
existingField.Attrs[model.PropertyAttrsProtected] = true
existingField.Attrs[model.PropertyAttrsAccessMode] = def.AccessMode
// See createField for why all three permission levels are set to sysadmin.
sysadmin := model.PermissionLevelSysadmin
existingField.PermissionField = &sysadmin
existingField.PermissionValues = &sysadmin
existingField.PermissionOptions = &sysadmin
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/sync/field_sync.go` around lines 108 - 116, The update block that
modifies existingField (inside updateField) is missing an update for the access
mode; add a line to set existingField.Attrs[model.PropertyAttrsAccessMode] from
the incoming field definition (e.g.
existingField.Attrs[model.PropertyAttrsAccessMode] =
def.Attrs[model.PropertyAttrsAccessMode] or, if the definition exposes
AccessMode directly, existingField.Attrs[model.PropertyAttrsAccessMode] =
def.AccessMode) so that access mode changes applied in createField are also
applied when updating existing fields.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant