Files
mostlymatter/server/channels/api4/custom_profile_attributes.go
Miguel de la Cruz ca34c6a03f Custom profile attributes field endpoints (#29662)
* Adds the main Property System Architecture components

This change adds the necessary migrations for the Property Groups,
Fields and Values tables to be created, the store layer and a Property
Service that can be used from the app layer.

* Adds Custom Profile Attributes endpoints and app layer

* implement get and patch cpa values

* run i18n-extract

* Update property field type to use user instead of person

* Update PropertyFields to allow for unique nondeleted fields and remove redundant indexes

* Update PropertyValues to allow for unique nondeleted fields and remove redundant indexes

* Use StringMap instead of the map[string]any on property fields

* Add i18n strings

* Revert "Use StringMap instead of the map[string]any on property fields"

This reverts commit e2735ab0f8589d2524d636419ca0cb144575c4d6.

* Cast JSON binary data to string and add todo note for StringMap use

* Add mocks to the retrylayer tests

* Cast JSON binary data to string in property value store

* Check for binary parameter instead of casting to string for JSON data

* Fix bad merge

* Check property field type is one of the allowed ones

* Avoid reusing err variable to be explicit about the returned value

* Merge Property System Migrations into one file

* Adds NOT NULL to timestamps at the DB level

* Update stores to use tableSelectQuery instead of a slice var

* Update PropertyField model translations to be more explicit and avoid repetition

* Update PropertyValue model translations to be more explicit and avoid repetition

* Use ExecBuilder instead of ToSql&Exec

* Update property field errors to add context

* Ensure PerPage is greater than zero

* Update store errors to give more context

* Use ExecBuilder in the property stores where possible

* Add an on conflict suffix to the group register to avoid race conditions

* Remove user profile API documentation changes

* Update patchCPAValues endpoint and docs to return the updated information

* Merge two similar error conditions

* Use a route function for ListCPAValues

* Remove badly used translation string

* Remove unused get in register group method

* Adds input sanitization and validation to the CPA API endpoints

* Takes login outside of one test case to make it clear it affects multiple t.Runs

* Fix wrap error and return code when property field has been deleted

* Fix receiver name

* Adds comment to move the CPA group ID to the db cache

* Set the PerPage of CPA fields to the fields limit

* Update server/channels/app/custom_profile_attributes_test.go

Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>

* Standardize group ID access

* Avoid polluting the state between tests

* Use specific errors for the retrieval of CPA group

---------

Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
2025-01-13 18:12:38 +01:00

219 строки
6.1 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
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/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) {
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
}
var pf *model.PropertyField
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
}
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
}
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 patchCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
var attributeValues map[string]string
if jsonErr := json.NewDecoder(r.Body).Decode(&attributeValues); jsonErr != nil {
c.SetInvalidParamWithErr("attrs", jsonErr)
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
}
auditRec := c.MakeAuditRecord("patchCPAValues", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", userID)
results := make(map[string]string)
for fieldID, value := range attributeValues {
patchedValue, appErr := c.App.PatchCPAValue(userID, fieldID, strings.TrimSpace(value))
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) {
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]string)
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))
}
}