mattermost/server/public/model/property_access.go
David Krauser 63c3c70fe9
[MM-66836] Add some access control mechanisms with a wrapper around the property service (#34812)
Custom profile attributes (properties) in Mattermost need to support security-critical use cases like Attribute-Based Access Control (ABAC), external identity system synchronization, and privacy-preserving collaboration. Without access controls on these properties, any user or component could modify property fields and values, making them unsuitable for security decisions. Additionally, different properties require different visibility patterns - some need to be publicly readable, some should only be visible to their managing system, and some require privacy-preserving visibility where users can only see shared values.

This change introduces the PropertyAccessService, a wrapper around PropertyService that enforces access control for all property operations. This service is introduced in isolation and is not yet hooked up to the Plugin API, REST API, or app layer. It provides the foundation for a single enforcement point that will apply access restrictions consistently across all code paths once integrated.
2026-02-06 16:21:51 -05:00

77 lines
2.3 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
)
const (
// Property Field Access Control Attributes
PropertyAttrsProtected = "protected"
PropertyAttrsSourcePluginID = "source_plugin_id"
PropertyAttrsAccessMode = "access_mode"
// Access Modes
PropertyAccessModePublic = "" // Empty string means public (default)
PropertyAccessModeSourceOnly = "source_only"
PropertyAccessModeSharedOnly = "shared_only"
)
// IsKnownPropertyAccessMode checks if the given access mode is a recognized value
func IsKnownPropertyAccessMode(accessMode string) bool {
switch accessMode {
case PropertyAccessModePublic,
PropertyAccessModeSourceOnly,
PropertyAccessModeSharedOnly:
return true
}
return false
}
// IsPropertyFieldProtected returns whether a PropertyField is protected from modifications
// by callers other than the source plugin
func IsPropertyFieldProtected(field *PropertyField) bool {
if field.Attrs == nil {
return false
}
protected, ok := field.Attrs[PropertyAttrsProtected].(bool)
return ok && protected
}
// ValidatePropertyFieldAccessMode validates that the access_mode attribute is valid
// and compatible with the field type
func ValidatePropertyFieldAccessMode(field *PropertyField) error {
if field.Attrs == nil {
return nil
}
accessMode, ok := field.Attrs[PropertyAttrsAccessMode].(string)
if !ok {
// No access mode set, that's fine (defaults to public)
return nil
}
// Check if access mode is known
if !IsKnownPropertyAccessMode(accessMode) {
return fmt.Errorf("invalid access mode '%s'", accessMode)
}
// Validate shared_only is only used with select/multiselect fields
if accessMode == PropertyAccessModeSharedOnly {
if field.Type != PropertyFieldTypeSelect && field.Type != PropertyFieldTypeMultiselect {
return fmt.Errorf("access mode 'shared_only' can only be used with select or multiselect field types, got '%s'", field.Type)
}
}
// Validate that non-public access modes require protected flag
if accessMode == PropertyAccessModeSourceOnly || accessMode == PropertyAccessModeSharedOnly {
if !IsPropertyFieldProtected(field) {
return fmt.Errorf("access mode '%s' requires the field to be protected", accessMode)
}
}
return nil
}