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]>
59 lines
1.8 KiB
Go
59 lines
1.8 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/require"
|
|
)
|
|
|
|
func TestAccessDecisionJSON(t *testing.T) {
|
|
t.Run("bare decision omits the context", func(t *testing.T) {
|
|
data, err := json.Marshal(AccessDecision{Decision: true})
|
|
require.NoError(t, err)
|
|
require.JSONEq(t, `{"decision":true}`, string(data))
|
|
})
|
|
|
|
t.Run("no-policy decision round-trips", func(t *testing.T) {
|
|
in := NewNoPolicyAccessDecision()
|
|
data, err := json.Marshal(in)
|
|
require.NoError(t, err)
|
|
require.JSONEq(t, `{"decision":true,"context":{"reason":"no_policy"}}`, string(data))
|
|
|
|
var out AccessDecision
|
|
require.NoError(t, json.Unmarshal(data, &out))
|
|
require.Equal(t, in, out)
|
|
require.True(t, out.IsNoPolicy())
|
|
})
|
|
}
|
|
|
|
func TestAccessDecisionIsNoPolicy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
decision AccessDecision
|
|
want bool
|
|
}{
|
|
{"no context", AccessDecision{Decision: true}, false},
|
|
{"no reason key", AccessDecision{Decision: true, Context: map[string]any{"other": "x"}}, false},
|
|
{"non-string reason", AccessDecision{Decision: true, Context: map[string]any{AccessDecisionContextKeyReason: 1}}, false},
|
|
{"unrelated reason", AccessDecision{Decision: true, Context: map[string]any{AccessDecisionContextKeyReason: "whatever"}}, false},
|
|
{"no_policy reason", NewNoPolicyAccessDecision(), true},
|
|
// A deny must never be classified as an unregulated request, however
|
|
// the context labels it.
|
|
{
|
|
"deny contradicting the no_policy reason",
|
|
AccessDecision{Decision: false, Context: map[string]any{AccessDecisionContextKeyReason: string(AccessDecisionReasonNoPolicy)}},
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
require.Equal(t, tc.want, tc.decision.IsNoPolicy())
|
|
})
|
|
}
|
|
}
|