diff --git a/AGENTS.md b/AGENTS.md index e29b4c6..01ccd60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,17 +4,17 @@ Detailed context for AI agents working on this codebase. ## What This Project Is -A **Mattermost plugin starter template** that synchronizes user profile attributes from external systems into Mattermost's Custom Profile Attributes (CPA). It's a reference implementation and educational resource — designed to be read, understood, and adapted. This is not a plugin that can be used as-is as a plug-and-play solution. It is expected that a developer takes this and uses it as the foundation of their own custom plugin. +A **Mattermost plugin starter template** that synchronizes user attributes from an external system into Mattermost. The synced attributes appear on user profiles in the UI and are also addressable as `user.attributes.` from attribute-based access control (ABAC) policy rules — which is why the plugin writes into the `access_control` property group. It's a reference implementation and educational resource — designed to be read, understood, and adapted. This is not a plugin that can be used as-is as a plug-and-play solution. It is expected that a developer takes this and uses it as the foundation of their own custom plugin. **Plugin ID:** `com.mattermost.user-attribute-sync-starter-template` -**Min Mattermost version:** 11.5.0 -**Languages:** Go 1.24+ (server), TypeScript/React (webapp) +**Min Mattermost version:** 11.8.0 +**Languages:** Go 1.26.3+ (server), TypeScript/React (webapp) ## Architecture ``` Plugin Activation (Once) - ├─> Create/Update CPA Fields (schema) + ├─> Create/Update User Attribute Fields (schema) └─> Start Background Job (cluster-aware) Background Job (Configurable interval, default 60min) @@ -24,7 +24,9 @@ Background Job (Configurable interval, default 60min) The plugin has two phases: 1. **Field sync** — Creates/updates field definitions (schema) in Mattermost on activation -2. **Value sync** — Periodically fetches user data from a provider and writes values to CPA +2. **Value sync** — Periodically fetches user data from a provider and writes per-user values + +All fields and values are stored in the `access_control` property group (`model.AccessControlPropertyGroupName`), with `ObjectType=user` and `TargetType=system`. Living in that group is what makes the fields addressable from ABAC policy expressions. ## Key Files and Their Roles @@ -36,7 +38,7 @@ The plugin has two phases: | `server/job.go` | Cluster-aware job scheduling via `cluster.Schedule()`. Contains `nextWaitInterval()` (calculates delay) and `runSync()` (executes sync). | | `server/configuration.go` | Thread-safe config management with RWMutex. Settings: `SyncIntervalMinutes` (default 60). | | `server/sync/provider.go` | `AttributeProvider` interface: `GetUserAttributes() ([]map[string]interface{}, error)` and `Close() error`. | -| `server/sync/field_sync.go` | Field definitions array and schema management. Creates/updates CPA fields. Maintains `FieldIDCache` mapping external names to Mattermost-generated IDs. | +| `server/sync/field_sync.go` | Field definitions array and schema management. Creates/updates user attribute fields. Maintains `FieldIDCache` mapping external names to Mattermost-generated IDs. | | `server/sync/value_sync.go` | `SyncUsers()` — matches users by email, builds PropertyValue objects, bulk upserts. Handles text, date, and multiselect value types. | | `server/sync/file_provider.go` | Example `AttributeProvider` implementation. Reads JSON from Mattermost data directory. Tracks file modification time for incremental sync. | | `server/main.go` | Plugin entry point (minimal). | @@ -61,7 +63,7 @@ The plugin has two phases: ## Field Definitions -Defined in `server/sync/field_sync.go` we have a few example field definitions in the `fieldDefinitions` array: +Defined in `server/sync/field_sync.go` we have a few example user attribute field definitions in the `fieldDefinitions` array: 1. **Job Title** — `job_title`, Text type, Public access 2. **Programs** — `programs`, Multiselect type, SharedOnly access, options: Apples/Oranges/Lemons/Grapes @@ -142,7 +144,7 @@ make logs-watch # Tail plugin logs on running server Used via `pluginapi.Client`: -- `Property.GetPropertyGroup(name)` — Fetch CPA group +- `Property.GetPropertyGroup(name)` — Fetch the `access_control` property group - `Property.GetPropertyFieldByName(groupID, objectID, fieldName)` — Lookup field by name - `Property.CreatePropertyField(field)` — Create field (returns generated ID) - `Property.UpdatePropertyField(groupID, field)` — Modify existing field diff --git a/README.md b/README.md index 119b6b1..7d34b94 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,21 @@ **This is a starter template, not a production-ready plugin. It is meant to be forked and adapted to your own external data source and field definitions. Do not install it as-is expecting a working integration.** -A Mattermost plugin starter template that demonstrates how to synchronize user profile attributes from external systems into Mattermost's Custom Profile Attributes (CPA). This template serves as both a working reference implementation and an educational resource for plugin developers. +A Mattermost plugin starter template that demonstrates how to synchronize user attributes from an external system into Mattermost. Synced attributes appear on user profiles in the UI and can also be referenced from attribute-based access control (ABAC) policy rules — this is why the plugin writes into the `access_control` property group rather than a plugin-private group. The template serves as both a working reference implementation and an educational resource for plugin developers. ## What This Template Demonstrates -Mattermost's Custom Profile Attributes system (also called Properties) allows you to store structured metadata about users. A **field** defines the schema (name, type, options), while a **value** stores the actual data for a specific user. For multiselect fields, **options** define the allowed choices that users can select from. +Mattermost's property system lets you store structured per-user metadata. A **field** defines the schema (name, type, options), while a **value** stores the actual data for a specific user. For multiselect fields, **options** define the allowed choices. Fields written into the `access_control` group also become available as `user.attributes.` inside ABAC policy expressions. -This plugin demonstrates how to create fields with hardcoded definitions and synchronize values from external data sources. Fields are defined explicitly in code with their types (text, date, multiselect), and the plugin uses Mattermost's cluster job system to run periodic synchronization tasks. The implementation includes incremental synchronization that processes only changed data after the initial sync. +This plugin shows how to create fields with hardcoded definitions and synchronize values from external data sources. Fields are defined explicitly in code with their types (text, date, multiselect), and the plugin uses Mattermost's cluster job system to run periodic synchronization tasks. The implementation includes incremental synchronization that processes only changed data after the initial sync. -The template creates three example fields that demonstrate different access control modes: Job Title (text, public access), Programs (multiselect with options, shared-only access), and Start Date (date, source-only access). All fields are marked as visible in the UI and protected (only this plugin can modify structure and write values). +The template creates three example user attribute fields that demonstrate different access control modes: Job Title (text, public access), Programs (multiselect with options, shared-only access), and Start Date (date, source-only access). All fields are marked as visible in the UI and protected (only this plugin can modify structure and write values). ## Architecture Overview ``` Plugin Activation (Once) - ├─> Create/Update CPA Fields + ├─> Create/Update User Attribute Fields └─> Start Background Job Background Job (On timed interval) @@ -35,8 +35,8 @@ Background Job (On timed interval) ### Prerequisites -- Mattermost server 11.5.0 or later -- Go 1.24 or later +- Mattermost server 11.8.0 or later +- Go 1.26.3 or later (matches the version Mattermost server pins) - Node v16 and npm v8 (if modifying webapp) ### Installation @@ -66,13 +66,13 @@ Background Job (On timed interval) ## What to Expect -When the plugin activates, it creates the three Custom Profile Attribute fields (Job Title, Programs, and Start Date) in Mattermost. These fields appear in System Console → User Attributes. If the fields already exist from a previous activation, the plugin updates them to match the hardcoded definitions. +When the plugin activates, it creates the three user attribute fields (Job Title, Programs, and Start Date) in Mattermost. These fields appear in System Console → User Attributes. If the fields already exist from a previous activation, the plugin updates them to match the hardcoded definitions. -Immediately after activation, the plugin runs its first synchronization. It reads the `user_attributes.json` file from the Mattermost data directory, matches users by email address, and populates the Custom Profile Attribute values for each user found in the data file. The plugin logs its progress and any errors (such as users not found in Mattermost) during this process. +Immediately after activation, the plugin runs its first synchronization. It reads the `user_attributes.json` file from the Mattermost data directory, matches users by email address, and populates the user attribute values for each user found in the data file. The plugin logs its progress and any errors (such as users not found in Mattermost) during this process. After the initial sync, the plugin checks for changes every 60 minutes by default. The file provider tracks the modification time of `user_attributes.json` and only processes the file if it has been modified since the last sync. When changes are detected, the plugin syncs all users in the file again. You can adjust the sync interval in the plugin configuration settings. -The synced attribute values are stored as Custom Profile Attributes and can be viewed in System Console → User Attributes or through the Mattermost API. +The synced user attribute values can be viewed in System Console → User Attributes or through the Mattermost API, and they are also available to ABAC policy rules as `user.attributes.`. ## Access Control diff --git a/go.mod b/go.mod index 2ed5a2f..70067e1 100644 --- a/go.mod +++ b/go.mod @@ -1,61 +1,60 @@ module github.com/mattermost/user-attribute-sync-starter-template -go 1.24.13 +go 1.26.3 require ( - github.com/mattermost/mattermost/server/public v0.1.23-0.20260213163939-568ab01e75ee + github.com/mattermost/mattermost/server/public v0.4.1 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 ) require ( + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/beevik/etree v1.6.0 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect - github.com/fatih/color v1.18.0 // indirect + github.com/fatih/color v1.19.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect - github.com/goccy/go-yaml v1.18.0 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.7.0 // indirect + github.com/hashicorp/go-plugin v1.8.0 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect - github.com/lib/pq v1.10.9 // indirect + github.com/lib/pq v1.12.3 // indirect github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect github.com/mattermost/gosaml2 v0.10.0 // indirect github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect github.com/mattermost/logr/v2 v2.0.22 // indirect - github.com/mattermost/mattermost/server/v8 v8.0.0-20251014075701-833e0125320d // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect github.com/oklog/run v1.2.0 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/russellhaering/goxmldsig v1.5.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/russellhaering/goxmldsig v1.6.0 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/stretchr/objx v0.5.3 // indirect - github.com/tinylib/msgp v1.4.0 // indirect + github.com/tinylib/msgp v1.6.4 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/wiggin77/merror v1.0.5 // indirect github.com/wiggin77/srslog v1.0.1 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect - google.golang.org/grpc v1.76.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.81.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 40e61ad..d4608f2 100644 --- a/go.sum +++ b/go.sum @@ -8,17 +8,19 @@ dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1 dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -30,8 +32,8 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64= github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= @@ -45,8 +47,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= @@ -81,8 +83,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= -github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= +github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= @@ -104,8 +106,8 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= @@ -116,10 +118,8 @@ github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI= github.com/mattermost/logr/v2 v2.0.22 h1:npFkXlkAWR9J8payh8ftPcCZvLbHSI125mAM5/r/lP4= github.com/mattermost/logr/v2 v2.0.22/go.mod h1:0sUKpO+XNMZApeumaid7PYaUZPBIydfuWZ0dqixXo+s= -github.com/mattermost/mattermost/server/public v0.1.23-0.20260213163939-568ab01e75ee h1:8RYnxQiaz+j5Pde5O+dUCFP/A61Qxj5/v5c2wWUltaY= -github.com/mattermost/mattermost/server/public v0.1.23-0.20260213163939-568ab01e75ee/go.mod h1:71/MHfP2k/s8fMa9GHqlX6je59Xg30JD8dWppT4EG/k= -github.com/mattermost/mattermost/server/v8 v8.0.0-20251014075701-833e0125320d h1:etRyN6FNd6fc7BGZ8X+XB2u/5Hb2HNz5/K53YZNvfrs= -github.com/mattermost/mattermost/server/v8 v8.0.0-20251014075701-833e0125320d/go.mod h1:HILhsra+xY4SNEFhuPbobH3I8a0aeXJcTJ6RWPX85nI= +github.com/mattermost/mattermost/server/public v0.4.1 h1:M15mKgz8b7EKIWcwYMNNWeluiTqnUPppR4VyTQUR660= +github.com/mattermost/mattermost/server/public v0.4.1/go.mod h1:2z08gasPXqIIbzl/xf2/2Sfn5ITFLGC6tplhOklyAAQ= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -128,8 +128,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -162,8 +162,8 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= -github.com/russellhaering/goxmldsig v1.5.0 h1:AU2UkkYIUOTyZRbe08XMThaOCelArgvNfYapcmSjBNw= -github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvDP6tE0nTaeUnjXDmk= +github.com/russellhaering/goxmldsig v1.6.0 h1:8fdWXEPh2k/NZNQBPFNoVfS3JmzS4ZprY/sAOpKQLks= +github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBeYAqaHEyUeWPxZlBM= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= @@ -188,8 +188,8 @@ github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1l github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -197,13 +197,12 @@ github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8= -github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o= +github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= +github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= @@ -215,31 +214,31 @@ github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRi github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -249,8 +248,8 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -272,16 +271,14 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -289,8 +286,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= @@ -303,16 +300,16 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/plugin.json b/plugin.json index 47044a5..b47d617 100644 --- a/plugin.json +++ b/plugin.json @@ -1,11 +1,11 @@ { "id": "com.mattermost.user-attribute-sync-starter-template", "name": "User Attribute Sync Starter Template", - "description": "A reference implementation demonstrating how to synchronize user profile attributes from external systems into Mattermost's Custom Profile Attributes.", + "description": "A reference implementation demonstrating how to synchronize user attributes from external systems into Mattermost, where they can be displayed on user profiles and referenced from attribute-based access control (ABAC) policies.", "homepage_url": "https://github.com/mattermost/mattermost-plugin-starter-template", "support_url": "https://github.com/mattermost/mattermost-plugin-starter-template/issues", "icon_path": "assets/starter-template-icon.svg", - "min_server_version": "11.5.0", + "min_server_version": "11.8.0", "server": { "executables": { "linux-amd64": "server/dist/plugin-linux-amd64", diff --git a/server/job.go b/server/job.go index fddaec2..479f711 100644 --- a/server/job.go +++ b/server/job.go @@ -42,7 +42,7 @@ func (p *Plugin) nextWaitInterval(now time.Time, metadata cluster.JobMetadata) t // runSync executes the user attribute value synchronization workflow. // // This function runs periodically (at the interval configured in plugin settings) to synchronize -// user attribute values from external sources into Mattermost Custom Profile Attributes. +// user attribute values from external sources into Mattermost user attribute fields. // // Note: Field schema synchronization (creating/updating PropertyFields) happens // once during plugin activation in OnActivate(). @@ -64,7 +64,7 @@ func (p *Plugin) runSync() { p.client.Log.Info("Fetched users for sync", "count", len(users)) // Sync user values using the field ID cache loaded during activation - err = sync.SyncUsers(p.client, p.cpaGroupID, users, p.fieldIDCache) + err = sync.SyncUsers(p.client, p.groupID, users, p.fieldIDCache) if err != nil { p.client.Log.Error("Failed to sync user values", "error", err.Error()) return diff --git a/server/plugin.go b/server/plugin.go index ec2806c..6dc709a 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -3,6 +3,7 @@ package main import ( "sync" + "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/pluginapi" "github.com/mattermost/mattermost/server/public/pluginapi/cluster" @@ -23,8 +24,11 @@ type Plugin struct { // fileProvider provides an example of syncing user attribute data from external source. fileProvider attrsync.AttributeProvider - // cpaGroupID is ID of the standard group used for Custom Profile Attributes - cpaGroupID string + // groupID is the ID of the Mattermost property group this plugin reads and writes. + // We use the "access_control" group because user attribute fields defined here can be + // referenced from attribute-based access control (ABAC) policy rules — e.g. a channel + // policy that only admits users whose "Programs" includes "Apples". + groupID string // fieldIDCache stores mappings from external field/option names to Mattermost-generated IDs fieldIDCache *attrsync.FieldIDCache @@ -41,17 +45,22 @@ type Plugin struct { func (p *Plugin) OnActivate() error { p.client = pluginapi.NewClient(p.API, p.Driver) - // "custom_profile_attributes" is the standard group name for Custom Profile Attributes. - // This group is automatically created by Mattermost core and is used for all CPA fields. - group, err := p.client.Property.GetPropertyGroup("custom_profile_attributes") + // "access_control" is the property group whose fields can be referenced + // from attribute-based access control (ABAC) policy rules. We register + // our user attributes here so policies can evaluate against them (e.g. + // "only admit users whose Programs includes Apples"). Mattermost core + // registers the group automatically on server startup; we just look it + // up here to get the group ID we'll write fields and values against. + group, err := p.client.Property.GetPropertyGroup(model.AccessControlPropertyGroupName) if err != nil { - return errors.Wrap(err, "failed to get Custom Profile Attributes group") + return errors.Wrap(err, "failed to get access_control property group") } - p.cpaGroupID = group.ID + p.groupID = group.ID - // Sync field definitions on plugin activation and load their IDs - // Creates/updates CPA fields and stores the auto-generated IDs for use during value sync. - p.fieldIDCache, err = attrsync.SyncFields(p.client, p.cpaGroupID, manifest.Id) + // Sync field definitions on plugin activation and load their IDs. + // Creates/updates the user attribute fields and stores the auto-generated + // IDs for use during value sync. + p.fieldIDCache, err = attrsync.SyncFields(p.client, p.groupID, manifest.Id) if err != nil { return errors.Wrap(err, "failed to sync field definitions") } @@ -62,7 +71,7 @@ func (p *Plugin) OnActivate() error { // Set up the attribute sync cluster job // This job runs periodically to synchronize user attribute values from external - // sources to Mattermost Custom Profile Attributes. Using cluster.Schedule ensures + // sources into Mattermost user attribute fields. Using cluster.Schedule ensures // only one server instance runs the job in multi-server deployments. job, err := cluster.Schedule( p.API, diff --git a/server/sync/field_sync.go b/server/sync/field_sync.go index f2d2350..787a866 100644 --- a/server/sync/field_sync.go +++ b/server/sync/field_sync.go @@ -27,12 +27,21 @@ func (c *FieldIDCache) GetOptionID(optionName string) string { return c.OptionNameToID[optionName] } -// fieldDefinition defines a Custom Profile Attribute field schema. +// fieldDefinition defines a user attribute field schema. type fieldDefinition struct { - Name string // Display name shown in UI - ExternalName string // Name used in external data source - Type model.PropertyFieldType // Field type (text, date, multiselect, etc.) - OptionNames []string // Option names for multiselect fields + // Name is the canonical field identifier. It must match ^[A-Za-z_][A-Za-z0-9_]*$ + // because Mattermost references the name from ABAC policy expressions as + // user.attributes. (a CEL identifier), so spaces and punctuation are + // rejected. We also use this name as the lookup key when matching attributes + // from the external data source — the JSON file's keys must match these names. + Name string + + // DisplayName is the human-readable label shown in user-facing UI. Free-form + // text; no character restrictions. + DisplayName string + + Type model.PropertyFieldType // Field type (text, date, multiselect, etc.) + OptionNames []string // Option names for multiselect fields // AccessMode controls who can read this field's values. Three modes: // - Public (empty string): Everyone can read all field options and values // - SourceOnly: Only this plugin can read values; others see empty options and no values @@ -43,9 +52,11 @@ type fieldDefinition struct { AccessMode string } -// fieldDefinitions contains all Custom Profile Attribute fields this plugin creates. -// Custom Profile Attributes (CPAs) are user metadata fields that appear in user profiles. -// This plugin ensures these fields exist on startup and syncs external data into them. +// fieldDefinitions contains all user attribute fields this plugin creates. +// These are per-user metadata fields stored in the access_control property +// group, so they appear on user profiles and can also be referenced from +// attribute-based access control (ABAC) policy rules. This plugin ensures +// these fields exist on startup and syncs external data into them. // // Access Control Examples: // The fields below demonstrate the three available access control modes. These are examples @@ -58,31 +69,31 @@ type fieldDefinition struct { var fieldDefinitions = []fieldDefinition{ { // Public Access Example: Job titles are visible to everyone in the organization - Name: "Job Title", - ExternalName: "job_title", - Type: model.PropertyFieldTypeText, - AccessMode: model.PropertyAccessModePublic, + Name: "job_title", + DisplayName: "Job Title", + Type: model.PropertyFieldTypeText, + AccessMode: model.PropertyAccessModePublic, }, { // Shared-Only Access Example: Users can only see programs they have in common // If viewing another user's profile, you'll only see programs you're both in - Name: "Programs", - ExternalName: "programs", - Type: model.PropertyFieldTypeMultiselect, - OptionNames: []string{"Apples", "Oranges", "Lemons", "Grapes"}, - AccessMode: model.PropertyAccessModeSharedOnly, + Name: "programs", + DisplayName: "Programs", + Type: model.PropertyFieldTypeMultiselect, + OptionNames: []string{"Apples", "Oranges", "Lemons", "Grapes"}, + AccessMode: model.PropertyAccessModeSharedOnly, }, { // Source-Only Access Example: Start dates are private - only this plugin can read them // Useful for data that should be synchronized but not visible to users or other systems - Name: "Start Date", - ExternalName: "start_date", - Type: model.PropertyFieldTypeDate, - AccessMode: model.PropertyAccessModeSourceOnly, + Name: "start_date", + DisplayName: "Start Date", + Type: model.PropertyFieldTypeDate, + AccessMode: model.PropertyAccessModeSourceOnly, }, } -// updateField updates an existing CPA field to match the definition. +// updateField updates an existing user attribute field to match the definition. // Returns the updated field. func updateField( client *pluginapi.Client, @@ -95,8 +106,14 @@ func updateField( "name", def.Name) 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 if def.Type == model.PropertyFieldTypeMultiselect { // Build options array with name only - Mattermost will generate IDs @@ -118,7 +135,7 @@ func updateField( return updatedField, nil } -// createField creates a new CPA field from the definition. +// createField creates a new user attribute field from the definition. // Returns the newly created field. func createField( client *pluginapi.Client, @@ -127,17 +144,63 @@ func createField( ) (*model.PropertyField, error) { client.Log.Info("Field does not exist, creating", "name", def.Name) + // These three permission levels describe who, role-wise, can edit the + // field definition (PermissionField), write a user's value + // (PermissionValues), or change the multiselect options (PermissionOptions). + // + // For this plugin they're largely a formality: every field we create is + // protected, so only the plugin can write through the source_plugin_id + // mechanism, and source_only/shared_only access modes already restrict + // who can read the values — even admins can't see source_only fields. + // Mattermost also pins PermissionField and PermissionOptions to sysadmin + // itself for any field in the access_control group, so those two are + // truly no-ops here. + // + // Even so, our shared_only field forces us to set PermissionValues. The + // default for user fields lets members edit their own value, and + // Mattermost rejects that combined with shared_only — if anyone could + // pick any value, they could fake having something in common with + // anyone. Setting PermissionValues to sysadmin clears that check. We + // set the other two to sysadmin alongside it so all three read the same + // way. + sysadmin := model.PermissionLevelSysadmin + field := &model.PropertyField{ // ID left empty - Mattermost will auto-generate - GroupID: groupID, - Name: def.Name, - Type: def.Type, + GroupID: groupID, + Name: def.Name, + Type: def.Type, + PermissionField: &sysadmin, + PermissionValues: &sysadmin, + PermissionOptions: &sysadmin, + + // ObjectType declares what kind of object this field describes. This is a + // user attribute sync plugin, so every field describes users and we pin + // ObjectType to "user". Mattermost uses this to route field queries — for + // example, the user-profile UI asks for fields with ObjectType=user, and + // ABAC policy evaluation looks up user.attributes. against the + // same set. + ObjectType: model.PropertyFieldObjectTypeUser, + + // TargetType declares the scope at which the field definition lives. + // "system" means the field is defined once globally and applies to every + // user on the server. The other options ("team", "channel") would scope + // the field to a specific team or channel, which is not what we want for + // org-wide profile attributes. With TargetType=system, TargetID must be + // empty (the system has no per-entity ID). + TargetType: string(model.PropertyFieldTargetLevelSystem), + Attrs: model.StringInterface{ + // DisplayName is the user-facing label rendered in profile cards and the + // System Console. Mattermost's Name field is a CEL identifier and can't + // contain spaces or punctuation, so anything human-readable lives here. + model.PropertyFieldAttrDisplayName: def.DisplayName, + // Visibility controls whether values appear in the UI (user profiles/cards). // This does NOT affect data access via API - use AccessMode for that. // "Always" makes values visible in the UI. "Hidden" hides them from UI but // data can still be retrieved via API (subject to AccessMode permissions). - model.CustomProfileAttributesPropertyAttrsVisibility: model.CustomProfileAttributesVisibilityAlways, + model.PropertyFieldAttrVisibility: model.PropertyFieldVisibilityAlways, // Protected means only this plugin can: // - Modify field structure (add/remove options, change field type) @@ -205,7 +268,7 @@ func isFieldOwnedByPlugin( return true } -// syncSingleField ensures a single CPA field exists and matches the definition. +// syncSingleField ensures a single user attribute field exists and matches the definition. // Updates the cache with field and option IDs. Returns the field ID or error. func syncSingleField( client *pluginapi.Client, @@ -240,7 +303,7 @@ func syncSingleField( } // Store the field name to ID mapping - cache.FieldNameToID[def.ExternalName] = field.ID + cache.FieldNameToID[def.Name] = field.ID // For multiselect fields, extract option IDs if def.Type == model.PropertyFieldTypeMultiselect && len(def.OptionNames) > 0 { @@ -302,7 +365,7 @@ func extractOptionIDs( return nil } -// SyncFields ensures all CPA fields exist and match the definitions. +// SyncFields ensures all user attribute fields exist and match the definitions. // Returns a FieldIDCache containing mappings from external names to Mattermost-generated IDs. // //nolint:revive diff --git a/server/sync/field_sync_test.go b/server/sync/field_sync_test.go index 7edadce..67976e9 100644 --- a/server/sync/field_sync_test.go +++ b/server/sync/field_sync_test.go @@ -21,18 +21,18 @@ func TestSyncFields(t *testing.T) { client := pluginapi.NewClient(api, &plugintest.Driver{}) // Mock GetPropertyFieldByName returning not found (fields don't exist yet) - api.On("GetPropertyFieldByName", groupID, "", "Job Title").Return(nil, errors.New("not found")).Once() - api.On("GetPropertyFieldByName", groupID, "", "Programs").Return(nil, errors.New("not found")).Once() - api.On("GetPropertyFieldByName", groupID, "", "Start Date").Return(nil, errors.New("not found")).Once() + api.On("GetPropertyFieldByName", groupID, "", "job_title").Return(nil, errors.New("not found")).Once() + api.On("GetPropertyFieldByName", groupID, "", "programs").Return(nil, errors.New("not found")).Once() + api.On("GetPropertyFieldByName", groupID, "", "start_date").Return(nil, errors.New("not found")).Once() // Mock field creation - ID is generated by Mattermost api.On("CreatePropertyField", mock.MatchedBy(func(f *model.PropertyField) bool { - return f.Name == "Job Title" && f.ID == "" - })).Return(&model.PropertyField{ID: "generated_id_1", Name: "Job Title", Type: model.PropertyFieldTypeText}, nil) + return f.Name == "job_title" && f.ID == "" + })).Return(&model.PropertyField{ID: "generated_id_1", Name: "job_title", Type: model.PropertyFieldTypeText}, nil) programsField := &model.PropertyField{ ID: "generated_id_2", - Name: "Programs", + Name: "programs", Type: model.PropertyFieldTypeMultiselect, Attrs: model.StringInterface{ model.PropertyFieldAttributeOptions: []interface{}{ @@ -43,12 +43,12 @@ func TestSyncFields(t *testing.T) { }, } api.On("CreatePropertyField", mock.MatchedBy(func(f *model.PropertyField) bool { - return f.Name == "Programs" && f.ID == "" + return f.Name == "programs" && f.ID == "" })).Return(programsField, nil) api.On("CreatePropertyField", mock.MatchedBy(func(f *model.PropertyField) bool { - return f.Name == "Start Date" && f.ID == "" - })).Return(&model.PropertyField{ID: "generated_id_3", Name: "Start Date", Type: model.PropertyFieldTypeDate}, nil) + return f.Name == "start_date" && f.ID == "" + })).Return(&model.PropertyField{ID: "generated_id_3", Name: "start_date", Type: model.PropertyFieldTypeDate}, nil) // Mock logging api.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe() @@ -75,13 +75,13 @@ func TestSyncFields(t *testing.T) { existingJobTitle := &model.PropertyField{ ID: "existing_id_1", GroupID: groupID, - Name: "Job Title", + Name: "job_title", Type: model.PropertyFieldTypeText, Attrs: model.StringInterface{ model.PropertyAttrsSourcePluginID: pluginID, }, } - api.On("GetPropertyFieldByName", groupID, "", "Job Title").Return(existingJobTitle, nil).Once() + api.On("GetPropertyFieldByName", groupID, "", "job_title").Return(existingJobTitle, nil).Once() api.On("UpdatePropertyField", groupID, mock.MatchedBy(func(f *model.PropertyField) bool { return f.ID == "existing_id_1" })).Return(existingJobTitle, nil).Once() @@ -89,7 +89,7 @@ func TestSyncFields(t *testing.T) { existingPrograms := &model.PropertyField{ ID: "existing_id_2", GroupID: groupID, - Name: "Programs", + Name: "programs", Type: model.PropertyFieldTypeMultiselect, Attrs: model.StringInterface{ model.PropertyAttrsSourcePluginID: pluginID, @@ -100,7 +100,7 @@ func TestSyncFields(t *testing.T) { }, }, } - api.On("GetPropertyFieldByName", groupID, "", "Programs").Return(existingPrograms, nil).Once() + api.On("GetPropertyFieldByName", groupID, "", "programs").Return(existingPrograms, nil).Once() api.On("UpdatePropertyField", groupID, mock.MatchedBy(func(f *model.PropertyField) bool { return f.ID == "existing_id_2" })).Return(existingPrograms, nil).Once() @@ -108,13 +108,13 @@ func TestSyncFields(t *testing.T) { existingStartDate := &model.PropertyField{ ID: "existing_id_3", GroupID: groupID, - Name: "Start Date", + Name: "start_date", Type: model.PropertyFieldTypeDate, Attrs: model.StringInterface{ model.PropertyAttrsSourcePluginID: pluginID, }, } - api.On("GetPropertyFieldByName", groupID, "", "Start Date").Return(existingStartDate, nil).Once() + api.On("GetPropertyFieldByName", groupID, "", "start_date").Return(existingStartDate, nil).Once() api.On("UpdatePropertyField", groupID, mock.MatchedBy(func(f *model.PropertyField) bool { return f.ID == "existing_id_3" })).Return(existingStartDate, nil).Once() @@ -143,7 +143,7 @@ func TestSyncFields(t *testing.T) { // Mock field creation - verify Programs field has options optionsVerified := false api.On("CreatePropertyField", mock.MatchedBy(func(f *model.PropertyField) bool { - if f.Name == "Programs" { + if f.Name == "programs" { // Verify Programs field has correct options (Apples, Oranges, Lemons, Grapes) options, ok := f.Attrs[model.PropertyFieldAttributeOptions].([]interface{}) if ok && len(options) == 4 { @@ -154,7 +154,7 @@ func TestSyncFields(t *testing.T) { })).Return(func(f *model.PropertyField) (*model.PropertyField, error) { // Return field with generated ID f.ID = "gen_id_" + f.Name - if f.Name == "Programs" { + if f.Name == "programs" { f.Attrs[model.PropertyFieldAttributeOptions] = []interface{}{ map[string]interface{}{"id": "opt1", "name": "Apples"}, map[string]interface{}{"id": "opt2", "name": "Oranges"}, @@ -193,7 +193,7 @@ func TestSyncFields(t *testing.T) { } // Subsequent calls succeed f.ID = "generated_id_" + f.Name - if f.Name == "Programs" { + if f.Name == "programs" { f.Attrs[model.PropertyFieldAttributeOptions] = []interface{}{ map[string]interface{}{"id": "opt1", "name": "Apples"}, } diff --git a/server/sync/provider.go b/server/sync/provider.go index 06e9ecd..8329c02 100644 --- a/server/sync/provider.go +++ b/server/sync/provider.go @@ -1,7 +1,7 @@ package sync // AttributeProvider defines the interface for data sources that provide user attributes -// to be synchronized into Mattermost's Custom Profile Attributes system. +// to be synchronized as Mattermost user attribute fields. // // The interface is designed to be stateless from the caller's perspective - the provider // implementation is responsible for tracking its own state (e.g., last read time, pagination diff --git a/server/sync/value_sync.go b/server/sync/value_sync.go index a0b0485..a357e76 100644 --- a/server/sync/value_sync.go +++ b/server/sync/value_sync.go @@ -132,7 +132,7 @@ func buildPropertyValues(api *pluginapi.Client, user *model.User, groupID string return values, nil } -// SyncUsers writes attribute values from external data into Mattermost CPAs for all users. +// SyncUsers writes attribute values from external data into Mattermost user attribute fields for all users. // //nolint:revive func SyncUsers(api *pluginapi.Client, groupID string, users []map[string]interface{}, cache *FieldIDCache) error {