Files
team/server/public/model/access_request_test.go
0668bdbdfa ABAC: plugin-keyed resource types, trusted plugin PAP/CEL APIs, and AuthZEN-style decision API (#37509)
* 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]>
2026-08-04 09:59:48 -04:00

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())
})
}
}