feat(auth): per-service team scope - #216
Open
TartanLeGrand wants to merge 13 commits into
Open
TartanLeGrand wants to merge 13 commits into
TartanLeGrand wants to merge 13 commits into
Conversation
authz.ScopeFromContext and authz.RequireService give RPC methods one place to read and enforce the service scope of the caller. store.ServiceFilter builds the matching MongoDB condition. A context without principal, an empty restricted scope and an empty service all fail closed. Scope denials are counted in tracker_auth_requests_total with result=scope_denied. Refs BananaOps#196
A team scope that sets scope_all together with services, or whose services are all blank, is now rejected instead of silently becoming every service. Scopes are bounded to 500 services of 128 characters. No service still means every service, and the built-in Administrators team keeps an immutable unrestricted scope. Tests pin Me and team API keys returning the effective scope. Refs BananaOps#196
Event lists, searches and statistics are filtered on attributes.service in MongoDB when the caller's scope is restricted. Reading, updating, deleting, commenting or linking a single event is refused with PermissionDenied when its service is outside the scope, and an update checks both the stored and the new service. Events without a service require an unrestricted scope. Nothing changes for callers whose scope is all. Refs BananaOps#196
ListLocks is filtered on the lock service for restricted callers. CreateLock, GetLock, UpdateLock and UnLock are refused with PermissionDenied outside the scope, UpdateLock checks both the stored and the new service, and a lock cannot be linked to an event outside the scope. The locks taken and released by CreateEvent and UpdateEvent stay covered by the event checks. Adds the idx_lock_service index. Refs BananaOps#196
UnLock of a lock in scope stays allowed, but the unlocked changelog entry is no longer written on a linked event outside the caller scope. Adds tests for the cross link, empty stored service, unknown event id and the non-unique idx_lock_service index. Refs BananaOps#196
Catalog entries are filtered and checked on their name. An in-scope entry is returned as stored, including the names of the services it depends on. Version compliance is computed on the scoped list: only in-scope projects are reported and a deliverable outside the scope is treated as absent, so its versions do not leak. Adds the idx_catalog_name index on the collection the catalog store really uses. Refs BananaOps#196
A table lists each RPC as filtered, checked or exempt with a reason, and must stay equal to the set of declared RPCs. Every non exempt RPC is called on real data with an empty scope: checked ones must answer PermissionDenied, filtered ones must return nothing. Custom HTTP routes are counted per file so a new one cannot ship without a stance. Refs BananaOps#196
Users and a team API key of two teams go through the real mux and middleware: lists are filtered, unit operations outside the scope answer 403, Me returns the scope, a scope change applies to live sessions and keys, and anonymous, global keys and administrators keep seeing everything. Refs BananaOps#196
Denied catalog, event and unlock requests now re-read MongoDB to prove nothing was written, and Me is checked for an explicit scopeAll false. Refs BananaOps#196
Explains what a scope restricts, how lists and unit operations answer, the rules for objects without a service and for moving an object across scopes, what is not scoped, the team API validation and the upgrade note for teams that already list services. Refs BananaOps#196
The 128 limit used the byte length while the docs and the error say characters, so multibyte names were rejected too early. Count runes instead. Also drop a process reference from a test comment. Refs BananaOps#196
Document that the anonymous caller keeps scope all, so team scoping only isolates once AUTH_ANONYMOUS_PERMISSIONS is restricted. Correct the statement on teams without services and re-wrap a long line. Refs BananaOps#196
The web team dialog (BananaOps#201) now sends back the scope it loaded, so editing a restricted team from the UI no longer widens it. Refs BananaOps#196
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Enforces the per-service team scope that
scope_all/scope_serviceson teams already stored but never applied (PR 4 of the SSO series). A caller now only sees and writes the events, locks and catalog entries of the services of their teams. Backend and docs only: no proto, no generated code, no web change.Commits:
feat(auth): add service scope helpersfeat(auth): validate team scope payloadsfeat(auth): scope events by servicefeat(auth): scope locks by servicefix(auth): do not write to out-of-scope events when unlockingfeat(auth): scope the catalog by servicetest(auth): guard every RPC and HTTP route against unscoped accesstest(auth): cover service scope end to end through the gatewaytest(auth): assert stored state after denied scope requestsdocs(auth): document the per-service team scopefix(auth): count team scope service names in charactersdocs(auth): warn that scope needs restricted anonymous accessSemantics
Full rules in
docs/AUTHENTICATION.md, section "Service scope".allas soon as one team isall; a team API key gets its team scope, re-read on every request. Global keys, anonymous callers andAdministratorsareall.PERMISSION_DENIED) naming the service; there is no 404 masking.all; a user who belongs to no team has an empty scope and sees and writes nothing./config.js, Swagger, AuthService (guarded byaccess:manage).scopeAll: truewith services is refused with 400, as is a list of blank services; names are trimmed and deduplicated (500 services of 128 characters max);Administratorscannot be restricted.GET /auth/mereturnsscopeAll/scopeServices.all, and whileAUTH_ANONYMOUS_PERMISSIONSkeeps its transitional default (every permission exceptaccess:manage), a restricted user or API key can read and write outside its scope by not sending its credential. SetAUTH_ANONYMOUS_PERMISSIONS=(empty), or read-only permissions (anonymous reads then stay unscoped). Documented indocs/AUTHENTICATION.md.all.tracker_auth_requests_total{result="scope_denied"}metric.Checks
go build ./...,go vet ./...: clean.gofmt -l: only the pre-existinginternal/utils/utils_test.go.go testwith packages run serially against a MongoDB 7 container: all packagesok(server 75s).-raceon./server/,./internal/stores/and./internal/auth/...: pass (counts of PASS lines include subtests).golangci-lint run ./...: 0 issues.gosec -exclude=G103,G115 -exclude-generated ./...: 0 issues.govulncheck ./...: 0 vulnerabilities in called code.git diff origin/main --stat -- '*_test.go'shows 10 new files, additions only.HandlePathroute and fails when one is neither scoped nor explicitly exempted.servwith a dedicated database: two teams scoped toservice-aandservice-b, two local users, a team-a API key. The user and the key see onlyservice-aevents, locks, catalog, stats and version compliance; get, update, delete and create onservice-banswer 403; moving an event toservice-banswers 403 and leaves it unchanged; a deploymentstarttakes theservice-alock andsuccessreleases it; widening team-a toservice-bapplies to the same key on the next request and narrowing it back restores the 403;GET /auth/mereports the scope; the admin sees everything;scope_deniedis counted for both user and API key principals andauthz deniedis logged.Notes for reviewers
List,Search, count and aggregate functions now take anauth.Scope. Open PR feat(integrations): record deployments from GitLab and Flux webhooks #209 (deploy integrations) will needauth.ScopeAll()in its calls at rebase, and must read the stored lock in theUpdateLockRPC before calling its core. Its webhook routes must be added to the guard table (httpRouteSites).idx_lock_serviceandidx_catalog_name.HandlePathcalls underserver/andcmd/.mustChangePassword, but the backend does not enforce it (only the web UI redirects), so the manual run did not need a password change.Follow-ups
related_idcan reference an out-of-scope event, which reveals that it exists and when it was created.mustChangePassword(only the web UI redirects): worth a separate hardening.Refs #196