Per-API-key scoped permissions (subtree, glob, classification) #103
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Motivation
Today's authorization model (
docs/Authorization.md,GatewayScopes) is flat and verb-only: an API key has scopes likeinvoke:read,invoke:write,invoke:secure,metadata:read,events:read. The interceptor checks scope membership before the service body runs, never inspects the tag address inside the command, the gobject id in a browse request, or any attribute metadata.That's correct for a fully-trusted single-tenant client. It's not enough once multiple consumers (an OtOpcUa server, a future Historian replacement, a third-party diagnostic tool, an integration harness) share one gw — each should be limited to its own slice of the Galaxy.
Use cases:
Area1/*) only.is_historized=trueattributes but write nothing.OperatorTags.*).security_classification <= Operate.Proposed model
Keep verb scopes (
invoke:read/invoke:write/invoke:secure/metadata:read/events:read) — that gating happens first, fail-closed, in the interceptor as today.Add an optional constraint set stored alongside
ApiKeyIdentity.Scopesand applied at the service layer (after the interceptor, before the worker call):Matching semantics:
*/?.*_subtreesentry OR a*_tag_globsentry passes that constraint.browse_subtreesfilters the result rather than rejecting the call.SubscribeBulk,AddItemBulk) returns per-tagPermissionDeniedin the result list, not a whole-call fail.Where the check lives
IConstraintEnforcerinvoked from the service body for:AddItem,AddItem2,AddItemBulk,SubscribeBulk,AdviseItemBulk,Write,Write2,WriteSecured,WriteSecured2. Resolves the target tag'sgobject_id+contained_path+security_classificationagainst the cached hierarchy (already in memory), then applies the constraint set.GalaxyRepository.DiscoverHierarchyandWatchDeployEventsfilter results tobrowse_subtrees. Depends on the subtree/filter mechanism from the companion issue.PermissionDenied(or whole-call for non-bulk). Distinct fromUnauthenticated(no key) and the interceptor's verb-scopePermissionDenied. The status detail names which constraint blocked the call.Storage and key minting
constraintsJSON column.mxgw-keyCLI grows flags:--read-subtree,--write-subtree,--read-tag-glob,--write-tag-glob,--max-write-classification,--browse-subtree,--read-alarm-only,--read-historized-only. All optional, all repeatable.Acceptance
ApiKeyIdentitycarries aConstraintsrecord; key store schema migrated additively.IConstraintEnforcerinvoked from the documented service-side hooks.DiscoverHierarchyandWatchDeployEventsfilter results whenbrowse_subtreesis set.PermissionDeniedfor constraint violations; non-bulk commands return whole-call.mxgw-keyCLI accepts every constraint flag listed above.docs/Authorization.mdrewritten to cover the verb-scope + constraint two-layer model.Dependencies
DiscoverHierarchysubtree+filter —browse_subtreesreuses that pipeline.gobject_id → contained_pathlookup synchronous to the request thread (already materialized).Out of scope
is_alarm/is_historized/max_write_classificationtoggles. Can be added later.Source
Surfaced during
lmxopcuaGalaxy → MxGateway migration planning. OtOpcUa enforces user-level ACLs at its own server layer, so for OtOpcUa specifically the flat-scope key it currently mints is sufficient — this feature is for other gw consumers that would benefit from blast-radius limits.Depends on #102 �
browse_subtreesis implemented by reusing the subtree+filter mechanism added there.Implemented in
b995c17(codex/fix-runtime-review-findings).Verification passed:
dotnet build src\MxGateway.Contracts\MxGateway.Contracts.csprojscripts\publish-client-proto-inputs.ps1clients\go\generate-proto.ps1clients\python\generate-proto.ps1gradle :mxgateway-client:generateProtodotnet test src\MxGateway.sln --no-restoredotnet test clients\dotnet\MxGateway.Client.sln --no-restorego test ./...python -m pytestcargo fmt --all --checkcargo test --workspacegradle testgit diff --check