mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* MM: add v0.5 plugin access control policy model, registry, and decision outcomes Co-authored-by: nick.misasi <[email protected]> * MM: add plugin access control PDP/PAP app-layer methods with fail-closed semantics Co-authored-by: nick.misasi <[email protected]> * MM: expose plugin access control API surface (EvaluateAccessControl + PAP/CEL methods) Co-authored-by: nick.misasi <[email protected]> * MM: add store-layer round-trip tests for v0.5 plugin access control policies Co-authored-by: nick.misasi <[email protected]> * MM: extract plugin access control app code into plugin_access_control.go Co-authored-by: nick.misasi <[email protected]> * MM: add atomic type-guarded AccessControlPolicyStore.DeleteIfType Co-authored-by: nick.misasi <[email protected]> * MM: plugin PAP hardening — atomic typed delete, indistinguishable 404s, audit every attempt Co-authored-by: nick.misasi <[email protected]> * MM: close plugin Get-by-ID TOCTOU via GetPolicyOfType; stamp save audit operation at entry Co-authored-by: nick.misasi <[email protected]> * Fix gob RPC poisoning from native attribute select options NativeUserAttributeFields stored bool-select options as []map[string]string inside PropertyField.Attrs (map[string]any). gob requires concrete types inside interface values to be registered, and []map[string]string is not registered in client_rpc.go, so encoding the GetAccessControlFieldsAutocomplete reply failed and net/rpc shut down the shared plugin API connection, breaking every subsequent plugin API call. Build the options from gob-registered containers ([]any/map[string]any) instead; JSON output is byte-identical. Add gob round-trip regression tests covering every plugin access control API reply payload: the autocomplete response including native attribute fields (fails against the old code), policies with JSON-decoded Props, visual AST condition values of every runtime shape, expression check errors, query users responses, and evaluation decisions. Co-authored-by: nick.misasi <[email protected]> * Move plugin access control gob-safety tests into their own file Pure move: plugin_access_control_test.go crossed 1000 lines; the gob-safety helper and TestPluginAccessControlGobSafety now live in plugin_access_control_gob_test.go, unchanged. Co-authored-by: nick.misasi <[email protected]> * MM: resolve plugin policy existence when ABAC is unavailable (Option B) Every evaluation-impossible branch of EvaluatePluginAccessRequest (service nil / unlicensed / flag off / user load or subject build failure / evaluator infra error / unknown outcome) now performs a raw open-core store read on the already-validated resource ID: no stored row returns no_policy so the caller can safely apply legacy behavior; any stored row (with a Warn on a foreign-type anomaly) or a failed read returns unavailable so the caller must fail closed. This lets the plugin drop its local policy index entirely. Strengthens the EvaluateAccessControl doc contract accordingly and reworks the fail-closed test matrix with with/without-row splits per branch, a foreign-type-row case, a store-read-error case on the store mock, and passthrough rows pinning that the fallback read never runs when the evaluator answers. Co-authored-by: nick.misasi <[email protected]> * Clarify EvaluateAccessControl failure-mapping doc A failure with a definitive store miss maps to no_policy under the Option B semantics, so 'failures never map to allow or no_policy' was inaccurate. State precisely: never allow; no_policy only on positively determined non-existence; deny for defensive failures on a resolved policy; unavailable otherwise. Co-authored-by: nick.misasi <[email protected]> * Tighten ABAC comments Co-authored-by: nick.misasi <[email protected]> * Document AccessDecision.Outcome as plugin-API-only The evaluator populates Outcome on every lane, but the only production reader is the app layer's EvaluatePluginAccessRequest, which maps it into PluginAccessControlDecision; core channel/team enforcement reads the collapsed Decision bool alone. State that on the field. Co-authored-by: nick.misasi <[email protected]> * Retrigger CI to rebuild enterprise image with updated enterprise branch Co-authored-by: nick.misasi <[email protected]> * Document that only the evaluator's plugin lane sets AccessDecision.Outcome Co-authored-by: nick.misasi <[email protected]> * ABAC: key plugin resource types as plugin_id:type and drop the static registry Co-authored-by: nick.misasi <[email protected]> * ABAC: replace AccessDecision.Outcome with the AuthZEN decision context reason Co-authored-by: nick.misasi <[email protected]> * ABAC: drop type-scoped policy get/delete and check the type in the app layer Co-authored-by: nick.misasi <[email protected]> * ABAC: compare plugin policy type ownership exactly instead of case-insensitively Co-authored-by: nick.misasi <[email protected]> * ABAC: bound the whole policy type to the Type column width Co-authored-by: nick.misasi <[email protected]> * ABAC: require an allow before treating a decision as the no-policy fallback Co-authored-by: nick.misasi <[email protected]> * ABAC: treat a colliding foreign-type policy row as no_policy, not a deny Co-authored-by: nick.misasi <[email protected]> * ABAC: gate plugin policy reads on a raw store read before normalization Co-authored-by: nick.misasi <[email protected]> * ABAC: pin policy type immutability on save in the store tests Co-authored-by: nick.misasi <[email protected]> * ABAC: reuse a single unavailable-error constructor in the existence fallback Co-authored-by: nick.misasi <[email protected]> * ABAC: re-check plugin policy ownership on the normalized get read Co-authored-by: nick.misasi <[email protected]> * ABAC: confirm plugin policy ownership before surfacing a normalization error Co-authored-by: nick.misasi <[email protected]> --------- Co-authored-by: Hanzo Dev <[email protected]>
67 lines
2.6 KiB
Go
67 lines
2.6 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package model
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNativeUserAttributeFields(t *testing.T) {
|
|
fields := NativeUserAttributeFields("group-1")
|
|
require.Len(t, fields, 4)
|
|
|
|
byName := map[string]*PropertyField{}
|
|
seenIDs := map[string]bool{}
|
|
for _, f := range fields {
|
|
assert.Equal(t, "group-1", f.GroupID)
|
|
assert.Equal(t, PropertyFieldObjectTypeUser, f.ObjectType)
|
|
|
|
// The ABAC editors resolve the selected attribute by ID, so each
|
|
// synthetic native field must carry a unique, non-empty ID.
|
|
assert.NotEmpty(t, f.ID, "field %q must have a non-empty ID", f.Name)
|
|
assert.False(t, seenIDs[f.ID], "field %q has a duplicate ID %q", f.Name, f.ID)
|
|
seenIDs[f.ID] = true
|
|
assert.Equal(t, true, f.Attrs[NativeAttributeAttrMarker], "field %q must be marked native", f.Name)
|
|
assert.NotEmpty(t, f.Attrs[NativeAttributeAttrDisplayName], "field %q must carry a display name", f.Name)
|
|
assert.NotEmpty(t, f.Attrs[NativeAttributeAttrOperators], "field %q must advertise operators", f.Name)
|
|
require.NotNil(t, f.PermissionField)
|
|
assert.Equal(t, PermissionLevelSysadmin, *f.PermissionField)
|
|
byName[f.Name] = f
|
|
}
|
|
|
|
t.Run("email is a text field with string operators", func(t *testing.T) {
|
|
f := byName[NativeAttributePropertyFieldEmail]
|
|
require.NotNil(t, f)
|
|
assert.Equal(t, PropertyFieldTypeText, f.Type)
|
|
assert.Equal(t, []string{"==", "!=", "in", "contains", "startsWith", "endsWith"}, f.Attrs[NativeAttributeAttrOperators])
|
|
assert.Nil(t, f.Attrs[PropertyFieldAttributeOptions])
|
|
})
|
|
|
|
for _, name := range []string{NativeAttributePropertyFieldVerified, NativeAttributePropertyFieldIsBot} {
|
|
t.Run(name+" is a boolean select", func(t *testing.T) {
|
|
f := byName[name]
|
|
require.NotNil(t, f)
|
|
assert.Equal(t, PropertyFieldTypeSelect, f.Type)
|
|
assert.Equal(t, []string{"==", "!="}, f.Attrs[NativeAttributeAttrOperators])
|
|
// []any/map[string]any so the field survives the plugin RPC
|
|
// boundary; the JSON shape is pinned below.
|
|
assert.Equal(t, []any{map[string]any{"name": "true"}, map[string]any{"name": "false"}}, f.Attrs[PropertyFieldAttributeOptions])
|
|
|
|
data, err := json.Marshal(f.Attrs[PropertyFieldAttributeOptions])
|
|
require.NoError(t, err)
|
|
assert.JSONEq(t, `[{"name":"true"},{"name":"false"}]`, string(data))
|
|
})
|
|
}
|
|
|
|
t.Run("createat only offers youngerThanDays", func(t *testing.T) {
|
|
f := byName[NativeAttributePropertyFieldCreateAt]
|
|
require.NotNil(t, f)
|
|
assert.Equal(t, []string{"youngerThanDays"}, f.Attrs[NativeAttributeAttrOperators])
|
|
})
|
|
}
|