* test: Add unit tests for custom profile attributes select options
* feat: Add custom profile attributes model with validation and constants
* refactor: Trim spaces from name and color in custom profile attribute select option constructor
* gofmt
* refactor: Fix typo in custom profile attributes select option function name
* feat: Add IsValid method to validate CustomProfileAttributesSelectOptions
* refactor: Replace map[string]bool with map[string]struct{} for key existence check
* refactor: Rename NewCustomProfileAttributeSelectOption to NewCustomProfileAttributesSelectOption
* feat: Add validation to prevent empty custom profile attribute options
* refactor: Add validation and creation methods for custom profile attributes
* feat: Add index number to validation error messages in custom profile attributes
* fix tests
* add default visibility
* feat: Add comprehensive test cases for custom profile attributes field validation
* fix: Update custom profile attributes map keys to use capitalized names
* feat: Add support for lowercase and title case keys in custom profile attributes map
* test: Add comprehensive test for NewCustomProfileAttributesSelectOptionFromMap
* feat: Add validation for custom profile attributes fields
* refactor: Update CustomProfileAttributesSelectOption constructor to prioritize ID parameter
* test: Add test cases for preserving IDs in custom profile attributes
* feat: Enhance ID validation and trimming in custom profile attributes
* don't do validation in constructor
* test: Add test case for preserving option IDs when patching select field
* improve test
* i18n
* refactor: Modify CustomProfileAttributesSelectOption to use lowercase JSON keys
* fix casing in custom profilte attributes test
* refactor: Use consistent "ValidateCPAField" in error messages for custom profile attributes
* use custom types rather than string
* lint
* fix api test
* refactor: Make color field optional in custom profile attributes
* style
* generic options
* removed unused i18n
* test: Add tests for NewCPAFieldFromPropertyField and CPAFieldToPropertyField
* test: Add test case for property field with empty attributes
* refactor: Cleanup whitespace and remove empty Attrs in custom profile attributes test
* test: Add test case for CPA field with empty attributes
* refactor: Improve custom profile attributes field handling and validation
* refactor: Move validateCustomProfileAttributesField to Validate method on CPAField struct
* use CPAField
* code style
* add validation and tests
* tests
* i18n
* err->appErr
* fix TestDeleteCPAField test
* i18n
* Add SAML and LDAP attr
* rename CustomProfileAttributes in method to CPA
* rename CPASortOrder method
* rearrange consts
* use Len test method
* sanitize and validate
* manage error the same way property field and value do
* fix: Update test error ID for custom profile attributes validation
* test: Update error ID expectations in custom profile attributes tests
* refactor: Convert CPAAttrs.SortOrder from string to int
* json uses float64
* feat: Add length validation for custom profile attribute option name and color
---------
Co-authored-by: Mattermost Build <build@mattermost.com>
351 строка
10 KiB
Go
351 строка
10 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package api4
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/v8/channels/audit"
|
|
)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func listCPAFields(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if c.App.Channels().License() == nil || !c.App.Channels().License().IsE20OrEnterprise() {
|
|
c.Err = model.NewAppError("Api4.listCPAFields", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
fields, appErr := c.App.ListCPAFields()
|
|
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 c.App.Channels().License() == nil || !c.App.Channels().License().IsE20OrEnterprise() {
|
|
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.SanitizeInput()
|
|
|
|
auditRec := c.MakeAuditRecord("createCPAField", audit.Fail)
|
|
defer c.LogAuditRec(auditRec)
|
|
audit.AddEventParameterAuditable(auditRec, "property_field", pf)
|
|
|
|
createdField, appErr := c.App.CreateCPAField(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 c.App.Channels().License() == nil || !c.App.Channels().License().IsE20OrEnterprise() {
|
|
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
|
|
}
|
|
|
|
patch.SanitizeInput()
|
|
|
|
auditRec := c.MakeAuditRecord("patchCPAField", audit.Fail)
|
|
defer c.LogAuditRec(auditRec)
|
|
audit.AddEventParameterAuditable(auditRec, "property_field_patch", patch)
|
|
|
|
originalField, appErr := c.App.GetCPAField(c.Params.FieldId)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
auditRec.AddEventPriorState(originalField)
|
|
|
|
patchedField, appErr := c.App.PatchCPAField(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 c.App.Channels().License() == nil || !c.App.Channels().License().IsE20OrEnterprise() {
|
|
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("deleteCPAField", audit.Fail)
|
|
defer c.LogAuditRec(auditRec)
|
|
audit.AddEventParameter(auditRec, "field_id", c.Params.FieldId)
|
|
|
|
field, appErr := c.App.GetCPAField(c.Params.FieldId)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
auditRec.AddEventPriorState(field)
|
|
|
|
if appErr := c.App.DeleteCPAField(c.Params.FieldId); appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
auditRec.AddEventResultState(field)
|
|
auditRec.AddEventObjectType("property_field")
|
|
|
|
ReturnStatusOK(w)
|
|
}
|
|
|
|
func sanitizePropertyValue(cpaField *model.CPAField, rawValue json.RawMessage) (json.RawMessage, error) {
|
|
fieldType := cpaField.Type
|
|
|
|
// build a list of existing options so we can check later if the values exist
|
|
optionsMap := map[string]struct{}{}
|
|
for _, v := range cpaField.Attrs.Options {
|
|
optionsMap[v.ID] = struct{}{}
|
|
}
|
|
|
|
switch fieldType {
|
|
case model.PropertyFieldTypeText, model.PropertyFieldTypeDate, model.PropertyFieldTypeSelect, model.PropertyFieldTypeUser:
|
|
var value string
|
|
if err := json.Unmarshal(rawValue, &value); err != nil {
|
|
return nil, err
|
|
}
|
|
value = strings.TrimSpace(value)
|
|
|
|
if fieldType == model.PropertyFieldTypeText {
|
|
if cpaField.Attrs.ValueType == model.CustomProfileAttributesValueTypeEmail && !model.IsValidEmail(value) {
|
|
return nil, fmt.Errorf("invalid email")
|
|
}
|
|
|
|
if cpaField.Attrs.ValueType == model.CustomProfileAttributesValueTypeURL {
|
|
_, err := url.Parse(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid url: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if fieldType == model.PropertyFieldTypeSelect && value != "" {
|
|
if _, ok := optionsMap[value]; !ok {
|
|
return nil, fmt.Errorf("option \"%s\" does not exist", value)
|
|
}
|
|
}
|
|
|
|
if fieldType == model.PropertyFieldTypeUser && value != "" && !model.IsValidId(value) {
|
|
return nil, fmt.Errorf("invalid user id")
|
|
}
|
|
return json.Marshal(value)
|
|
|
|
case model.PropertyFieldTypeMultiselect, model.PropertyFieldTypeMultiuser:
|
|
var values []string
|
|
if err := json.Unmarshal(rawValue, &values); err != nil {
|
|
return nil, err
|
|
}
|
|
filteredValues := make([]string, 0, len(values))
|
|
for _, v := range values {
|
|
trimmed := strings.TrimSpace(v)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if fieldType == model.PropertyFieldTypeMultiselect {
|
|
if _, ok := optionsMap[v]; !ok {
|
|
return nil, fmt.Errorf("option \"%s\" does not exist", v)
|
|
}
|
|
}
|
|
|
|
if fieldType == model.PropertyFieldTypeMultiuser && !model.IsValidId(trimmed) {
|
|
return nil, fmt.Errorf("invalid user id: %s", trimmed)
|
|
}
|
|
filteredValues = append(filteredValues, trimmed)
|
|
}
|
|
return json.Marshal(filteredValues)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown field type: %s", fieldType)
|
|
}
|
|
}
|
|
|
|
func patchCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if c.App.Channels().License() == nil || !c.App.Channels().License().IsE20OrEnterprise() {
|
|
c.Err = model.NewAppError("Api4.patchCPAValues", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// This check is unnecessary for now
|
|
// Will be required when/if admins can patch other's values
|
|
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("patchCPAValues", audit.Fail)
|
|
defer c.LogAuditRec(auditRec)
|
|
audit.AddEventParameter(auditRec, "user_id", userID)
|
|
|
|
// Get all fields at once and build a map for quick lookup
|
|
allFields, appErr := c.App.ListCPAFields()
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
fieldMap := make(map[string]*model.PropertyField)
|
|
for _, field := range allFields {
|
|
fieldMap[field.ID] = field
|
|
}
|
|
|
|
results := make(map[string]json.RawMessage, len(updates))
|
|
for fieldID, rawValue := range updates {
|
|
field, ok := fieldMap[fieldID]
|
|
if !ok {
|
|
c.Err = model.NewAppError("Api4.patchCPAValues", "api.custom_profile_attributes.field_not_found", nil, "", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
cpaField, err := model.NewCPAFieldFromPropertyField(field)
|
|
if err != nil {
|
|
c.Err = model.NewAppError("Api4.patchCPAValues", "api.custom_profile_attributes.field_conversion_error", nil, "", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
sanitizedValue, err := sanitizePropertyValue(cpaField, rawValue)
|
|
if err != nil {
|
|
c.SetInvalidParam(fmt.Sprintf("value for field %s: %v", fieldID, err))
|
|
return
|
|
}
|
|
|
|
patchedValue, appErr := c.App.PatchCPAValue(userID, fieldID, sanitizedValue)
|
|
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 c.App.Channels().License() == nil || !c.App.Channels().License().IsE20OrEnterprise() {
|
|
c.Err = model.NewAppError("Api4.listCPAValues", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
c.RequireUserId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
userID := c.Params.UserId
|
|
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext, c.AppContext.Session().UserId, userID)
|
|
if err != nil || !canSee {
|
|
c.SetPermissionError(model.PermissionViewMembers)
|
|
return
|
|
}
|
|
|
|
values, appErr := c.App.ListCPAValues(userID)
|
|
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))
|
|
}
|
|
}
|