Files
upstream-mattermost/server/channels/app/properties/property_value.go
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

345 lines
11 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package properties
import (
"fmt"
"net/http"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
// rejectTemplateValues checks that none of the given values target a template
// field. Template fields are definition-only and must never hold values.
// This is enforced at the service layer to cover all entry points (API,
// CPA endpoints, plugin API).
func (ps *PropertyService) rejectTemplateValues(rctx request.CTX, values []*model.PropertyValue) error {
// Collect unique field IDs
seen := make(map[string]struct{}, len(values))
for _, v := range values {
if v == nil {
continue
}
seen[v.FieldID] = struct{}{}
}
if len(seen) == 0 {
return nil
}
fieldIDs := make([]string, 0, len(seen))
for id := range seen {
fieldIDs = append(fieldIDs, id)
}
// Batch lookup from master to avoid replication lag
fields, err := ps.fieldStore.GetMany(store.RequestContextWithMaster(rctx), "", fieldIDs)
if err != nil {
return fmt.Errorf("failed to look up fields for template check: %w", err)
}
for _, field := range fields {
if field.ObjectType == model.PropertyFieldObjectTypeTemplate {
return model.NewAppError(
"PropertyService",
"app.property_value.template_no_values.app_error",
nil,
fmt.Sprintf("template field %q cannot have values", field.ID),
http.StatusBadRequest,
)
}
}
return nil
}
// Private implementation methods (database access)
func (ps *PropertyService) createPropertyValue(rctx request.CTX, value *model.PropertyValue) (*model.PropertyValue, error) {
if err := ps.rejectTemplateValues(rctx, []*model.PropertyValue{value}); err != nil {
return nil, err
}
return ps.valueStore.Create(value)
}
func (ps *PropertyService) createPropertyValues(rctx request.CTX, values []*model.PropertyValue) ([]*model.PropertyValue, error) {
if err := ps.rejectTemplateValues(rctx, values); err != nil {
return nil, err
}
return ps.valueStore.CreateMany(values)
}
func (ps *PropertyService) getPropertyValue(groupID, id string) (*model.PropertyValue, error) {
return ps.valueStore.Get(groupID, id)
}
func (ps *PropertyService) getPropertyValues(groupID string, ids []string) ([]*model.PropertyValue, error) {
return ps.valueStore.GetMany(groupID, ids)
}
func (ps *PropertyService) searchPropertyValues(groupID string, opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) {
// groupID is part of the search method signature to
// incentivize the use of the database indexes in searches
opts.GroupID = groupID
return ps.valueStore.SearchPropertyValues(opts)
}
func (ps *PropertyService) updatePropertyValue(rctx request.CTX, groupID string, value *model.PropertyValue) (*model.PropertyValue, error) {
values, err := ps.updatePropertyValues(rctx, groupID, []*model.PropertyValue{value})
if err != nil {
return nil, err
}
return values[0], nil
}
func (ps *PropertyService) updatePropertyValues(rctx request.CTX, groupID string, values []*model.PropertyValue) ([]*model.PropertyValue, error) {
if err := ps.rejectTemplateValues(rctx, values); err != nil {
return nil, err
}
return ps.valueStore.Update(groupID, values)
}
func (ps *PropertyService) upsertPropertyValue(rctx request.CTX, value *model.PropertyValue) (*model.PropertyValue, error) {
values, err := ps.upsertPropertyValues(rctx, []*model.PropertyValue{value})
if err != nil {
return nil, err
}
return values[0], nil
}
func (ps *PropertyService) upsertPropertyValues(rctx request.CTX, values []*model.PropertyValue) ([]*model.PropertyValue, error) {
if err := ps.rejectTemplateValues(rctx, values); err != nil {
return nil, err
}
return ps.valueStore.Upsert(values)
}
func (ps *PropertyService) deletePropertyValue(groupID, id string) error {
return ps.valueStore.Delete(groupID, id)
}
func (ps *PropertyService) deletePropertyValuesForTarget(groupID string, targetType string, targetID string) error {
return ps.valueStore.DeleteForTarget(groupID, targetType, targetID)
}
func (ps *PropertyService) deletePropertyValuesForField(groupID, fieldID string) error {
return ps.valueStore.DeleteForField(groupID, fieldID)
}
// Public methods
func (ps *PropertyService) CreatePropertyValue(rctx request.CTX, value *model.PropertyValue) (*model.PropertyValue, error) {
if value == nil {
return nil, fmt.Errorf("CreatePropertyValue: value cannot be nil")
}
processed, err := ps.runPreCreatePropertyValue(rctx, value)
if err != nil {
return nil, fmt.Errorf("CreatePropertyValue: %w", err)
}
created, err := ps.createPropertyValue(rctx, processed)
if err != nil {
return nil, err
}
ps.runPostCreatePropertyValue(rctx, created)
return created, nil
}
func (ps *PropertyService) CreatePropertyValues(rctx request.CTX, values []*model.PropertyValue) ([]*model.PropertyValue, error) {
if len(values) == 0 {
return values, nil
}
for i, v := range values {
if v == nil {
return nil, fmt.Errorf("CreatePropertyValues: nil element at index %d", i)
}
if v.GroupID != values[0].GroupID {
return nil, fmt.Errorf("CreatePropertyValues: mixed group IDs in batch")
}
}
processed, err := ps.runPreCreatePropertyValues(rctx, values)
if err != nil {
return nil, fmt.Errorf("CreatePropertyValues: %w", err)
}
created, err := ps.createPropertyValues(rctx, processed)
if err != nil {
return nil, err
}
ps.runPostCreatePropertyValues(rctx, created)
return created, nil
}
func (ps *PropertyService) GetPropertyValue(rctx request.CTX, groupID, id string) (*model.PropertyValue, error) {
value, err := ps.getPropertyValue(groupID, id)
if err != nil {
return nil, fmt.Errorf("GetPropertyValue: %w", err)
}
return ps.runPostGetPropertyValue(rctx, value)
}
func (ps *PropertyService) GetPropertyValues(rctx request.CTX, groupID string, ids []string) ([]*model.PropertyValue, error) {
values, err := ps.getPropertyValues(groupID, ids)
if err != nil {
return nil, fmt.Errorf("GetPropertyValues: %w", err)
}
return ps.runPostGetPropertyValues(rctx, values)
}
func (ps *PropertyService) SearchPropertyValues(rctx request.CTX, groupID string, opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) {
values, err := ps.searchPropertyValues(groupID, opts)
if err != nil {
return nil, fmt.Errorf("SearchPropertyValues: %w", err)
}
return ps.runPostGetPropertyValues(rctx, values)
}
func (ps *PropertyService) UpdatePropertyValue(rctx request.CTX, groupID string, value *model.PropertyValue) (*model.PropertyValue, error) {
processed, err := ps.runPreUpdatePropertyValue(rctx, groupID, value)
if err != nil {
return nil, fmt.Errorf("UpdatePropertyValue: %w", err)
}
updated, err := ps.updatePropertyValue(rctx, groupID, processed)
if err != nil {
return nil, err
}
ps.runPostUpdatePropertyValue(rctx, updated)
return updated, nil
}
func (ps *PropertyService) UpdatePropertyValues(rctx request.CTX, groupID string, values []*model.PropertyValue) ([]*model.PropertyValue, error) {
if len(values) == 0 {
return values, nil
}
// Hooks gate on values[0].GroupID for batch operations, so enforce
// single-group batches at the public boundary — otherwise a mixed
// batch could silently bypass per-group hook logic (license,
// validation, access control).
for i, v := range values {
if v == nil {
return nil, fmt.Errorf("UpdatePropertyValues: nil element at index %d", i)
}
if v.GroupID != values[0].GroupID {
return nil, fmt.Errorf("UpdatePropertyValues: mixed group IDs in batch")
}
}
processed, err := ps.runPreUpdatePropertyValues(rctx, groupID, values)
if err != nil {
return nil, fmt.Errorf("UpdatePropertyValues: %w", err)
}
updated, err := ps.updatePropertyValues(rctx, groupID, processed)
if err != nil {
return nil, err
}
ps.runPostUpdatePropertyValues(rctx, updated)
return updated, nil
}
func (ps *PropertyService) UpsertPropertyValue(rctx request.CTX, value *model.PropertyValue) (*model.PropertyValue, error) {
if value == nil {
return nil, fmt.Errorf("UpsertPropertyValue: value cannot be nil")
}
processed, err := ps.runPreUpsertPropertyValue(rctx, value)
if err != nil {
return nil, fmt.Errorf("UpsertPropertyValue: %w", err)
}
upserted, err := ps.upsertPropertyValue(rctx, processed)
if err != nil {
return nil, err
}
ps.runPostUpsertPropertyValue(rctx, upserted)
return upserted, nil
}
func (ps *PropertyService) UpsertPropertyValues(rctx request.CTX, values []*model.PropertyValue) ([]*model.PropertyValue, error) {
if len(values) == 0 {
return values, nil
}
for i, v := range values {
if v == nil {
return nil, fmt.Errorf("UpsertPropertyValues: nil element at index %d", i)
}
if v.GroupID != values[0].GroupID {
return nil, fmt.Errorf("UpsertPropertyValues: mixed group IDs in batch")
}
}
processed, err := ps.runPreUpsertPropertyValues(rctx, values)
if err != nil {
return nil, fmt.Errorf("UpsertPropertyValues: %w", err)
}
upserted, err := ps.upsertPropertyValues(rctx, processed)
if err != nil {
return nil, err
}
ps.runPostUpsertPropertyValues(rctx, upserted)
return upserted, nil
}
func (ps *PropertyService) DeletePropertyValue(rctx request.CTX, groupID, id string) error {
// Snapshot before the gates so post-hooks have the target/field metadata
// the row ID alone does not carry, and so a denied delete is observable.
// A genuine miss (ErrNotFound) yields a nil snapshot; a real read failure
// (e.g. replica lag or a transient error) is logged so it does not silently
// suppress the delete audit with incomplete metadata.
deleted, snapshotErr := ps.getPropertyValue(groupID, id)
if snapshotErr != nil && !store.IsErrNotFound(snapshotErr) {
rctx.Logger().Warn("DeletePropertyValue: failed to snapshot value before delete; audit metadata may be incomplete",
mlog.String("group_id", groupID),
mlog.String("value_id", id),
mlog.Err(snapshotErr),
)
}
if err := ps.runPreDeletePropertyValue(rctx, groupID, id); err != nil {
return fmt.Errorf("DeletePropertyValue: %w", err)
}
if err := ps.deletePropertyValue(groupID, id); err != nil {
return err
}
ps.runPostDeletePropertyValue(rctx, groupID, id, deleted)
return nil
}
func (ps *PropertyService) DeletePropertyValuesForTarget(rctx request.CTX, groupID string, targetType string, targetID string) error {
if err := ps.runPreDeletePropertyValuesForTarget(rctx, groupID, targetType, targetID); err != nil {
return fmt.Errorf("DeletePropertyValuesForTarget: %w", err)
}
if err := ps.deletePropertyValuesForTarget(groupID, targetType, targetID); err != nil {
return err
}
ps.runPostDeletePropertyValuesForTarget(rctx, groupID, targetType, targetID)
return nil
}
func (ps *PropertyService) DeletePropertyValuesForField(rctx request.CTX, groupID, fieldID string) error {
if err := ps.runPreDeletePropertyValuesForField(rctx, groupID, fieldID); err != nil {
return fmt.Errorf("DeletePropertyValuesForField: %w", err)
}
if err := ps.deletePropertyValuesForField(groupID, fieldID); err != nil {
return err
}
ps.runPostDeletePropertyValuesForField(rctx, groupID, fieldID)
return nil
}