Files
team/server/public/model/property_access.go

201 lines
7.1 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"fmt"
"regexp"
)
const (
// Property Field Access Control Attributes
PropertyAttrsProtected = "protected"
PropertyAttrsSourcePluginID = "source_plugin_id"
PropertyAttrsAccessMode = "access_mode"
PropertyAttrsOwners = "owners"
// Access Modes
PropertyAccessModePublic = "" // Empty string means public (default)
PropertyAccessModeSourceOnly = "source_only"
PropertyAccessModeSharedOnly = "shared_only"
)
// Property owner types. An owner is an identity trusted to manage an
// attribute's data; see the Property Permissions Proposal.
const (
PropertyOwnerTypePlugin = "plugin"
PropertyOwnerTypeService = "service"
PropertyOwnerTypeRole = "role"
PropertyOwnerTypeUser = "user"
)
// Defensive bounds on the owners list. These are not semantic limits — the
// server never interprets owner IDs or scopes — they only keep a buggy or
// hostile owner from bloating the field's Attrs blob. Real usage is far below
// them (a field has one or two owners, each with a handful of scopes). Scope
// labels must match the identifier charset enforced by IsValidPropertyOwnerScope.
const (
PropertyOwnersMaxPerField = 20 // owners per field
PropertyOwnerScopesMax = 32 // scopes per owner
PropertyOwnerScopeMaxRunes = 64 // length of a single scope label
PropertyOwnerIDMaxRunes = 255 // length of an owner id
)
// PropertyOwner is an identity trusted to manage an attribute's data. When a
// field carries one or more owners (in its Attrs blob under PropertyAttrsOwners)
// the owners list governs the write-access decision for that field, superseding
// the legacy protected / source_plugin_id gating and the sync-lock:
// - A machine caller must be a listed owner to write values or edit/delete the
// field. A listed owner may edit the whole field definition including the
// owners list (no own-entry restriction). A non-owner machine caller cannot
// modify the field or add itself; the first owner is bootstrapped by a
// sysadmin via the REST API.
// - Value writes by a machine caller are allowed only if it is a listed owner
// (matching ID and Type) whose Scopes contain the caller's acting-as scope.
// An owner with an empty Scopes list is not scope-restricted and may write
// for any scope.
// - Value writes by a human (session) caller are always rejected: an
// owner-managed field's values are authoritative to the owning
// integration, so no session user — including sysadmins — may overwrite
// them. This is enforced in the property-service hook, mirroring the
// ldap/saml sync lock; PermissionValues is left at the normal default.
//
// What it does NOT change: source_plugin_id immutability and the protected-flag
// rules still apply as invariants on field-definition writes (a plugin-created
// owner field still carries its source_plugin_id), and read/masking is unchanged
// and still keys off access_mode.
type PropertyOwner struct {
ID string `json:"id"`
Type string `json:"type"`
Scopes []string `json:"scopes"`
}
// IsValidPropertyOwnerType reports whether the given owner type is recognized.
func IsValidPropertyOwnerType(ownerType string) bool {
switch ownerType {
case PropertyOwnerTypePlugin,
PropertyOwnerTypeService,
PropertyOwnerTypeRole,
PropertyOwnerTypeUser:
return true
}
return false
}
var validPropertyOwnerScopeChars = regexp.MustCompile(`^[a-zA-Z0-9._:-]+$`)
// IsValidPropertyOwnerScope reports whether a scope label is identifier-shaped.
// This is a structural bound (like the length/count caps), not a semantic
// registry: the server still does not interpret what a scope means.
func IsValidPropertyOwnerScope(scope string) bool {
return validPropertyOwnerScopeChars.MatchString(scope)
}
// GetPropertyFieldOwners returns the owners declared on a field's Attrs blob.
// Returns nil when the field has no owners. Handles both the typed
// ([]PropertyOwner) shape used right after a write and the generic
// ([]interface{} of maps) shape produced by a JSON round-trip from the store.
func GetPropertyFieldOwners(field *PropertyField) []PropertyOwner {
if field == nil || field.Attrs == nil {
return nil
}
raw, ok := field.Attrs[PropertyAttrsOwners]
if !ok || raw == nil {
return nil
}
if owners, ok := raw.([]PropertyOwner); ok {
return owners
}
data, err := json.Marshal(raw)
if err != nil {
return nil
}
var owners []PropertyOwner
if err := json.Unmarshal(data, &owners); err != nil {
return nil
}
return owners
}
// HasPropertyFieldOwners reports whether a field declares any owners.
func HasPropertyFieldOwners(field *PropertyField) bool {
return len(GetPropertyFieldOwners(field)) > 0
}
// 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
}
// GetAccessMode returns the field's access mode. Returns the public mode (empty
// string) when no access_mode is configured or the field has no attrs at all.
func (f *PropertyField) GetAccessMode() string {
if f.Attrs == nil {
return PropertyAccessModePublic
}
accessMode, ok := f.Attrs[PropertyAttrsAccessMode].(string)
if !ok {
return PropertyAccessModePublic
}
return accessMode
}
// 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 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)
}
}
// shared_only + member-writable is contradictory: shared_only filters what
// callers see to values they hold, but member-writable lets users self-assign
// any value. Reject the combination at validation time instead of working
// around it at the API/service layer.
if accessMode == PropertyAccessModeSharedOnly && field.PermissionValues != nil && *field.PermissionValues == PermissionLevelMember {
return fmt.Errorf("access mode 'shared_only' is incompatible with member-writable permission_values")
}
return nil
}