mirror of
https://github.com/mattermost/mattermost.git
synced 2026-04-13 04:57:45 -04:00
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-external-links (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
* Refactor property system with app layer routing and access control separation Establish the app layer as the primary entry point for property operations with intelligent routing based on group type. This architecture separates access-controlled operations (CPA groups) from standard operations, improving performance and code clarity. Architecture Changes: - App layer now routes operations based on group type: - CPA groups -> PropertyAccessService (enforces access control) - Non-CPA groups -> PropertyService (direct, no access control) - PropertyAccessService simplified to handle only CPA operations - Eliminated redundant group type checks throughout the codebase * Move access control routing into PropertyService This change makes the PropertyService the main entrypoint for property related operations, and adds a routing mechanism to decide if extra behaviors or checks should run for each operation, in this case, the property access service logic. To add specific payloads that pluggable checks and operations may need, we use the request context. When the request comes from the API, the endpoints are in charge of adding the caller ID to the payload, and in the case of the plugin API, on receiving a request, the server automatically tags the context with the plugin ID so the property service can react accordingly. Finally, the new design enforces all these checks migrating the actual property logic to internal, non-exposed methods, so any caller from the App layer needs to go through the service checks that decide if pluggable logic is needed, avoiding any possibility of a bypass. * Fix i18n * Fix bad error string * Added nil guards to property methods * Add check for multiple group IDs on value operations * Add nil guard to the plugin checker * Fix build error * Update value tests * Fix linter * Adds early return when content flaggin a thread with no replies * Fix mocks * Clean the state of plugin property tests before each run * Do not wrap appErr on API response and fix i18n * Fix create property field test * Remove the need to cache cpaGroupID as part of the property service * Split the property.go file into multiple * Not found group doesn't bypass access control check * Unexport SetPluginCheckerForTests * Rename plugin context getter to be more PSA specific --------- Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
381 lines
12 KiB
Go
381 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"
|
|
"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))
|
|
}
|
|
}
|