Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
238 lines
6.8 KiB
Go
238 lines
6.8 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package commands
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"strings"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
|
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var UserAttributesCmd = &cobra.Command{
|
|
Use: "attributes",
|
|
Aliases: []string{"attrs", "cpa"},
|
|
Short: "Management of User Attributes",
|
|
Long: "Management of User Attributes fields and values.",
|
|
}
|
|
|
|
var UserAttributesFieldCmd = &cobra.Command{
|
|
Use: "field",
|
|
Short: "Management of User Attributes fields",
|
|
Long: "Create, list, edit, and delete User Attribute fields.",
|
|
}
|
|
|
|
var UserAttributesValueCmd = &cobra.Command{
|
|
Use: "value",
|
|
Short: "Management of User Attributes values",
|
|
Long: "List, set, and delete User Attribute values for users.",
|
|
}
|
|
|
|
func init() {
|
|
UserAttributesCmd.AddCommand(
|
|
UserAttributesFieldCmd,
|
|
UserAttributesValueCmd,
|
|
)
|
|
|
|
UserCmd.AddCommand(UserAttributesCmd)
|
|
}
|
|
|
|
// Helper function to build field attributes from command flags. If existingAttrs is
|
|
// provided, it will be used as the base and merged with flag changes
|
|
func buildFieldAttrs(cmd *cobra.Command, existingAttrs model.StringInterface) (model.StringInterface, error) {
|
|
var attrs = make(model.StringInterface)
|
|
if existingAttrs != nil {
|
|
maps.Copy(attrs, existingAttrs)
|
|
}
|
|
|
|
// First parse --attrs if provided
|
|
if attrsStr, err := cmd.Flags().GetString("attrs"); err == nil && attrsStr != "" && cmd.Flags().Changed("attrs") {
|
|
var attrsMap map[string]any
|
|
if err := json.Unmarshal([]byte(attrsStr), &attrsMap); err != nil {
|
|
return nil, fmt.Errorf("failed to parse attrs JSON: %w", err)
|
|
}
|
|
// Copy to our attrs map
|
|
maps.Copy(attrs, attrsMap)
|
|
}
|
|
|
|
// Individual flags override --attrs (applied on top)
|
|
if cmd.Flags().Changed("managed") {
|
|
managed, _ := cmd.Flags().GetBool("managed")
|
|
if managed {
|
|
attrs["managed"] = "admin"
|
|
} else {
|
|
attrs["managed"] = ""
|
|
}
|
|
}
|
|
|
|
// Handle --option flags for select/multiselect fields
|
|
if options, err := cmd.Flags().GetStringSlice("option"); err == nil && len(options) > 0 && cmd.Flags().Changed("option") {
|
|
var selectOptions []*model.CustomProfileAttributesSelectOption
|
|
|
|
existingOptionsMap := make(map[string]*model.CustomProfileAttributesSelectOption)
|
|
if existingOptions, ok := attrs["options"]; ok {
|
|
existingOptionsJSON, err := json.Marshal(existingOptions)
|
|
if err == nil {
|
|
var existingSelectOptions []*model.CustomProfileAttributesSelectOption
|
|
if err := json.Unmarshal(existingOptionsJSON, &existingSelectOptions); err == nil {
|
|
for _, option := range existingSelectOptions {
|
|
existingOptionsMap[option.Name] = option
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, optionName := range options {
|
|
if existingOption, exists := existingOptionsMap[optionName]; exists {
|
|
selectOptions = append(selectOptions, existingOption)
|
|
} else {
|
|
selectOptions = append(selectOptions, &model.CustomProfileAttributesSelectOption{
|
|
ID: model.NewId(),
|
|
Name: optionName,
|
|
})
|
|
}
|
|
}
|
|
attrs["options"] = selectOptions
|
|
}
|
|
|
|
return attrs, nil
|
|
}
|
|
|
|
func hasAttrsChanges(cmd *cobra.Command) bool {
|
|
return cmd.Flags().Changed("managed") ||
|
|
cmd.Flags().Changed("attrs") ||
|
|
cmd.Flags().Changed("option")
|
|
}
|
|
|
|
func getFieldFromArg(c client.Client, fieldArg string) (*model.PropertyField, error) {
|
|
fields, _, err := c.ListCPAFields(context.TODO())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get CPA fields: %w", err)
|
|
}
|
|
|
|
if model.IsValidId(fieldArg) {
|
|
for _, field := range fields {
|
|
if field.ID == fieldArg {
|
|
return field, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, field := range fields {
|
|
if field.Name == fieldArg {
|
|
return field, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to get field for %q", fieldArg)
|
|
}
|
|
|
|
// setupCPATemplateContext sets up template functions for field and value resolution
|
|
func setupCPATemplateContext(c client.Client) error {
|
|
// Get all fields once for the entire command
|
|
fields, _, err := c.ListCPAFields(context.TODO())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get CPA fields for template context: %w", err)
|
|
}
|
|
|
|
fieldMap := make(map[string]*model.PropertyField)
|
|
for _, field := range fields {
|
|
fieldMap[field.ID] = field
|
|
}
|
|
|
|
// Set template function to resolve field ID to field name
|
|
printer.SetTemplateFunc("fieldName", func(fieldID string) string {
|
|
if field, exists := fieldMap[fieldID]; exists {
|
|
return field.Name
|
|
}
|
|
return fieldID // fallback to field ID if not found
|
|
})
|
|
|
|
// Set template function to get field type
|
|
printer.SetTemplateFunc("fieldType", func(fieldID string) string {
|
|
if field, exists := fieldMap[fieldID]; exists {
|
|
return string(field.Type)
|
|
}
|
|
return "unknown"
|
|
})
|
|
|
|
// Set template function to resolve field value to human-readable format
|
|
printer.SetTemplateFunc("resolveValue", func(fieldID string, rawValue json.RawMessage) string {
|
|
field, exists := fieldMap[fieldID]
|
|
if !exists {
|
|
return string(rawValue)
|
|
}
|
|
|
|
return resolveDisplayValue(field, rawValue)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// resolveDisplayValue converts raw field values to human-readable display format
|
|
func resolveDisplayValue(field *model.PropertyField, rawValue json.RawMessage) string {
|
|
if field.Type.SupportsOptions() {
|
|
return resolveOptionDisplayValue(field, rawValue)
|
|
}
|
|
|
|
var value any
|
|
if err := json.Unmarshal(rawValue, &value); err != nil {
|
|
return string(rawValue)
|
|
}
|
|
return fmt.Sprintf("%v", value)
|
|
}
|
|
|
|
// resolveOptionDisplayValue converts option IDs to option names for select/multiselect fields
|
|
func resolveOptionDisplayValue(field *model.PropertyField, rawValue json.RawMessage) string {
|
|
// Convert PropertyField to CPAField to access options
|
|
cpaField, err := model.NewCPAFieldFromPropertyField(field)
|
|
if err != nil {
|
|
return string(rawValue)
|
|
}
|
|
|
|
if len(cpaField.Attrs.Options) == 0 {
|
|
return string(rawValue)
|
|
}
|
|
|
|
// Create option lookup map
|
|
optionMap := make(map[string]string)
|
|
for _, option := range cpaField.Attrs.Options {
|
|
optionMap[option.ID] = option.Name
|
|
}
|
|
|
|
if field.Type == model.PropertyFieldTypeSelect || field.Type == model.PropertyFieldTypeRank {
|
|
// Single select - expect a string
|
|
var optionID string
|
|
if err := json.Unmarshal(rawValue, &optionID); err != nil {
|
|
return string(rawValue)
|
|
}
|
|
if optionName, exists := optionMap[optionID]; exists {
|
|
return optionName
|
|
}
|
|
return optionID
|
|
}
|
|
|
|
// Multiselect - expect an array
|
|
var optionIDs []string
|
|
if err := json.Unmarshal(rawValue, &optionIDs); err != nil {
|
|
return string(rawValue)
|
|
}
|
|
|
|
optionNames := make([]string, 0, len(optionIDs))
|
|
for _, optionID := range optionIDs {
|
|
if optionName, exists := optionMap[optionID]; exists {
|
|
optionNames = append(optionNames, optionName)
|
|
} else {
|
|
optionNames = append(optionNames, optionID)
|
|
}
|
|
}
|
|
return fmt.Sprintf("[%s]", strings.Join(optionNames, ", "))
|
|
}
|