Files
team/server/channels/api4/custom_profile_attributes.go
JG HeithcockandGitHub 35fb70282d MM-68235: Rename user-visible "Custom Profile Attributes" to "User Attributes" (#36046)
* MM-68235: Rename user-visible "Custom Profile Attributes" to "User Attributes"

Update all English i18n translation values and inline
defaultMessage strings to use the current product name
"User Attributes" instead of the old "Custom Profile
Attributes" / CPA naming.

Add naming-history comments to key CPA source files
(model, app, api4, admin component) explaining that
internal identifiers retain the old naming for backward
compatibility with REST APIs, WebSocket events, and the
Property System Architecture group name. This helps
future developers understand the mapping without needing
to track down the rename history.

* Fix missed lowercase "custom profile attribute" strings
2026-04-13 08:13:58 -07:00

386 lines
12 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// This file implements the "User Attributes" API handlers (formerly "Custom
// Profile Attributes" / CPA). Internal identifiers and URL paths retain the
// old naming for backward compatibility. See MM-68235.
package api4
import (
"encoding/json"
"net/http"
"strings"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/app"
)
func (api *API) InitCustomProfileAttributes() {
if api.srv.Config().FeatureFlags.CustomProfileAttributes {
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APISessionRequired(listCPAFields)).Methods(http.MethodGet)
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APISessionRequired(createCPAField)).Methods(http.MethodPost)
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APISessionRequired(patchCPAField)).Methods(http.MethodPatch)
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APISessionRequired(deleteCPAField)).Methods(http.MethodDelete)
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(listCPAValues)).Methods(http.MethodGet)
api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APISessionRequired(patchCPAValues)).Methods(http.MethodPatch)
api.BaseRoutes.CustomProfileAttributes.Handle("/group", api.APISessionRequired(getCPAGroup)).Methods(http.MethodGet)
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(patchCPAValuesForUser)).Methods(http.MethodPatch)
}
}
func listCPAFields(c *Context, w http.ResponseWriter, r *http.Request) {
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
c.Err = model.NewAppError("Api4.listCPAFields", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
return
}
rctx := app.RequestContextWithCallerID(c.AppContext, c.AppContext.Session().UserId)
fields, appErr := c.App.ListCPAFields(rctx)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(fields); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createCPAField(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
c.Err = model.NewAppError("Api4.createCPAField", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
return
}
var pf *model.CPAField
err := json.NewDecoder(r.Body).Decode(&pf)
if err != nil || pf == nil {
c.SetInvalidParamWithErr("property_field", err)
return
}
pf.Name = strings.TrimSpace(pf.Name)
auditRec := c.MakeAuditRecord(model.AuditEventCreateCPAField, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterAuditableToAuditRec(auditRec, "property_field", pf)
rctx := app.RequestContextWithCallerID(c.AppContext, c.AppContext.Session().UserId)
createdField, appErr := c.App.CreateCPAField(rctx, pf)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(createdField)
auditRec.AddEventObjectType("property_field")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(createdField); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchCPAField(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
c.Err = model.NewAppError("Api4.patchCPAField", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
return
}
c.RequireFieldId()
if c.Err != nil {
return
}
var patch *model.PropertyFieldPatch
err := json.NewDecoder(r.Body).Decode(&patch)
if err != nil || patch == nil {
c.SetInvalidParamWithErr("property_field_patch", err)
return
}
if patch.Name != nil {
*patch.Name = strings.TrimSpace(*patch.Name)
}
if err := patch.IsValid(); err != nil {
if appErr, ok := err.(*model.AppError); ok {
c.Err = appErr
} else {
c.Err = model.NewAppError("createCPAField", "api.custom_profile_attributes.invalid_field_patch", nil, "", http.StatusBadRequest)
}
return
}
auditRec := c.MakeAuditRecord(model.AuditEventPatchCPAField, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterAuditableToAuditRec(auditRec, "property_field_patch", patch)
rctx := app.RequestContextWithCallerID(c.AppContext, c.AppContext.Session().UserId)
originalField, appErr := c.App.GetCPAField(rctx, c.Params.FieldId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventPriorState(originalField)
patchedField, appErr := c.App.PatchCPAField(rctx, c.Params.FieldId, patch)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(patchedField)
auditRec.AddEventObjectType("property_field")
if err := json.NewEncoder(w).Encode(patchedField); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteCPAField(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
c.Err = model.NewAppError("Api4.deleteCPAField", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
return
}
c.RequireFieldId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventDeleteCPAField, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "field_id", c.Params.FieldId)
rctx := app.RequestContextWithCallerID(c.AppContext, c.AppContext.Session().UserId)
field, appErr := c.App.GetCPAField(rctx, c.Params.FieldId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventPriorState(field)
if appErr := c.App.DeleteCPAField(rctx, c.Params.FieldId); appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(field)
auditRec.AddEventObjectType("property_field")
ReturnStatusOK(w)
}
func getCPAGroup(c *Context, w http.ResponseWriter, r *http.Request) {
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
c.Err = model.NewAppError("Api4.getCPAGroup", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
return
}
groupID, appErr := c.App.CpaGroupID()
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(map[string]string{"id": groupID}); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
c.Err = model.NewAppError("Api4.patchCPAValues", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
return
}
userID := c.AppContext.Session().UserId
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
var updates map[string]json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
c.SetInvalidParamWithErr("value", err)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventPatchCPAValues, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "user_id", userID)
// if the user is not an admin, we need to check that there are no
// admin-managed fields
session := *c.AppContext.Session()
rctx := app.RequestContextWithCallerID(c.AppContext, session.UserId)
if !c.App.SessionHasPermissionTo(session, model.PermissionManageSystem) {
fields, appErr := c.App.ListCPAFields(rctx)
if appErr != nil {
c.Err = appErr
return
}
// Check if any of the fields being updated are admin-managed
for _, field := range fields {
if _, isBeingUpdated := updates[field.ID]; isBeingUpdated {
if field.IsAdminManaged() {
c.Err = model.NewAppError("Api4.patchCPAValues", "app.custom_profile_attributes.property_field_is_managed.app_error", nil, "", http.StatusForbidden)
return
}
}
}
}
results := make(map[string]json.RawMessage, len(updates))
for fieldID, rawValue := range updates {
patchedValue, appErr := c.App.PatchCPAValue(rctx, userID, fieldID, rawValue, false)
if appErr != nil {
c.Err = appErr
return
}
results[fieldID] = patchedValue.Value
}
auditRec.Success()
auditRec.AddEventObjectType("patchCPAValues")
if err := json.NewEncoder(w).Encode(results); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func listCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
c.Err = model.NewAppError("Api4.listCPAValues", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
return
}
c.RequireUserId()
if c.Err != nil {
return
}
targetUserID := c.Params.UserId
callerUserID := c.AppContext.Session().UserId
// we check unrestricted sessions to allow local mode requests to go through
if !c.AppContext.Session().IsUnrestricted() {
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext, callerUserID, targetUserID)
if err != nil || !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
}
rctx := app.RequestContextWithCallerID(c.AppContext, callerUserID)
values, appErr := c.App.ListCPAValues(rctx, targetUserID)
if appErr != nil {
c.Err = appErr
return
}
returnValue := make(map[string]json.RawMessage)
for _, value := range values {
returnValue[value.FieldID] = value.Value
}
if err := json.NewEncoder(w).Encode(returnValue); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchCPAValuesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
c.Err = model.NewAppError("Api4.patchCPAValuesForUser", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
return
}
// Get userID from URL
c.RequireUserId()
if c.Err != nil {
return
}
userID := c.Params.UserId
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
var updates map[string]json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
c.SetInvalidParamWithErr("value", err)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventPatchCPAValues, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "user_id", userID)
// Check for admin-managed fields
session := *c.AppContext.Session()
rctx := app.RequestContextWithCallerID(c.AppContext, session.UserId)
isAdmin := c.App.SessionHasPermissionTo(session, model.PermissionManageSystem)
if !isAdmin {
fields, appErr := c.App.ListCPAFields(rctx)
if appErr != nil {
c.Err = appErr
return
}
for _, field := range fields {
if _, isBeingUpdated := updates[field.ID]; !isBeingUpdated {
continue
}
// Check for admin-managed fields
if field.IsAdminManaged() {
c.Err = model.NewAppError("Api4.patchCPAValuesForUser",
"app.custom_profile_attributes.property_field_is_managed.app_error",
nil, "",
http.StatusForbidden)
return
}
}
}
results := make(map[string]json.RawMessage, len(updates))
for fieldID, rawValue := range updates {
patchedValue, appErr := c.App.PatchCPAValue(rctx, userID, fieldID, rawValue, false)
if appErr != nil {
c.Err = appErr
return
}
results[fieldID] = patchedValue.Value
}
auditRec.Success()
auditRec.AddEventObjectType("patchCPAValues")
if err := json.NewEncoder(w).Encode(results); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}