Files
zeekay 8feb24ea1f One name for this fork, and it is ours
The server module named github.com/hanzoai/team/server/v8 — a path that
resolves to the retired workspace's repository, not this one — while
server/public still published github.com/mattermost/mattermost/server/public.
Both are github.com/hanzoteam/server/... now, which is where this code
actually lives.

That second half is what made the binary unbootable. Upstream's public path
stayed claimed by upstream, mattermost-plugin-ai imported it, and the build
linked public/model twice; public/plugin's gob.Register then ran twice under
one name and panicked at init, before main. Reverting the rename traded the
brand for a working binary. Rebranding the plugin alongside it needs no such
trade: the agents fork is github.com/hanzoteam/agents/v2 against our public
module, so no module named mattermost/mattermost is in the graph at all.

Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and
it is ours.
2026-08-28 23:35:36 -07:00

346 lines
11 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package properties
import (
"context"
"fmt"
"net/http"
"github.com/hanzoteam/server/server/public/model"
"github.com/hanzoteam/server/server/public/shared/mlog"
"github.com/hanzoteam/server/server/public/shared/request"
"github.com/hanzoteam/server/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(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.WithMaster(context.Background()), "", 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(value *model.PropertyValue) (*model.PropertyValue, error) {
if err := ps.rejectTemplateValues([]*model.PropertyValue{value}); err != nil {
return nil, err
}
return ps.valueStore.Create(value)
}
func (ps *PropertyService) createPropertyValues(values []*model.PropertyValue) ([]*model.PropertyValue, error) {
if err := ps.rejectTemplateValues(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(groupID string, value *model.PropertyValue) (*model.PropertyValue, error) {
values, err := ps.updatePropertyValues(groupID, []*model.PropertyValue{value})
if err != nil {
return nil, err
}
return values[0], nil
}
func (ps *PropertyService) updatePropertyValues(groupID string, values []*model.PropertyValue) ([]*model.PropertyValue, error) {
if err := ps.rejectTemplateValues(values); err != nil {
return nil, err
}
return ps.valueStore.Update(groupID, values)
}
func (ps *PropertyService) upsertPropertyValue(value *model.PropertyValue) (*model.PropertyValue, error) {
values, err := ps.upsertPropertyValues([]*model.PropertyValue{value})
if err != nil {
return nil, err
}
return values[0], nil
}
func (ps *PropertyService) upsertPropertyValues(values []*model.PropertyValue) ([]*model.PropertyValue, error) {
if err := ps.rejectTemplateValues(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(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(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(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(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(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(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
}