Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
376 lines
12 KiB
Go
376 lines
12 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"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
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 !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)
|
|
|
|
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 !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)
|
|
|
|
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 !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)
|
|
|
|
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 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, err := c.App.CpaGroupID()
|
|
if err != nil {
|
|
c.Err = model.NewAppError("Api4.getCPAGroup", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
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
|
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
|
fields, appErr := c.App.ListCPAFields()
|
|
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 {
|
|
// Convert to CPAField to check if managed
|
|
cpaField, fErr := model.NewCPAFieldFromPropertyField(field)
|
|
if fErr != nil {
|
|
c.Err = model.NewAppError("Api4.patchCPAValues", "app.custom_profile_attributes.property_field_conversion.app_error", nil, "", http.StatusInternalServerError).Wrap(fErr)
|
|
return
|
|
}
|
|
if cpaField.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(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
|
|
}
|
|
|
|
userID := c.Params.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, 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))
|
|
}
|
|
}
|
|
|
|
func patchCPAValuesForUser(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
|
|
}
|
|
|
|
// 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)
|
|
|
|
// if the user is not an admin, we need to check that there are no
|
|
// admin-managed fields
|
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
|
fields, appErr := c.App.ListCPAFields()
|
|
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 {
|
|
// Convert to CPAField to check if managed
|
|
cpaField, fErr := model.NewCPAFieldFromPropertyField(field)
|
|
if fErr != nil {
|
|
c.Err = model.NewAppError("Api4.patchCPAValues", "app.custom_profile_attributes.property_field_conversion.app_error", nil, "", http.StatusInternalServerError).Wrap(fErr)
|
|
return
|
|
}
|
|
if cpaField.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(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))
|
|
}
|
|
}
|