GitHub
11/07/2026, 9:04 PMnone, tags, standard or full.
• The tier is stored as a Tier column on the existing query/carve permission rows. AutoMigrate adds it; there is no data migration.
• Existing Query=true / Carve=true rows read as standard, so nobody loses access on upgrade.
• Env admins and superusers are full.
• Legacy boolean writes (CLI, old clients) work as before: true grants standard or keeps the current tier, and false sets none.
• When the boolean and the tier disagree, the boolean wins.
Tag grants. A new user_scope_tags table holds the tags a tags-tier user is scoped to.
• Tags are set through POST /users/{u}/permissions with query_tier, carve_tier and scope_tags.
• A tag from another environment gets a 400.
• The bulk endpoint rejects scope_tags.
• Deleting a tag removes its grants.
One node filter. handlers.NodeScope (pkg/handlers/scope.go) turns a tier into a SQL filter on osquery_nodes.
• It fails closed: none, an empty tag list or an unknown tier match nothing.
• A deleted tag never grants.
Targeting. POST /queries/{env} and POST /carves/{env} only create node-queries for nodes inside the caller's tier.
• Each node is judged in its own environment, which covers cross-environment targets.
• A node named explicitly by UUID or hostname that is out of scope gets 404 node not found, the same response as a missing node, and nothing is created.
• Raw carve SQL through the query endpoint needs full carve tier in the route environment and in every target environment.
Reads.
• Query results: JSON and CSV, from both the DB and S3 log readers, return only rows from visible nodes. An empty filter means no rows, never all rows.
• Carves: show, list and archive hide files from out-of-scope nodes. Explicit UUID/hostname targets are blanked below standard.
• Node detail, node lists (active/inactive/all) and posture: follow the caller's query/carve tier. A node outside it gets the same 404 as a missing one. node_key stays env-admin only.
• Plain user level: still gets the paged node list and node logs.
• Read-only admin: reads queries, carves and nodes at standard in every environment, never runs anything, and gets the redacted environment view.
Cancellation. Deleting a tag cancels pending node-queries whose creator reached the node only through that tag. Results already collected are hidden at read time.
Alert admin. Log-content rules (result_log, query_log, status_log) can be created, updated or deleted by a caller with full query tier in the rule's environment. They are never global, except for superusers.
• Updates check the stored environment.
• A rule's environment can't be changed (400).
• Channels, history and reveal stay superuser-only.
Docs and clients.
• OpenAPI is regenerated.
• The MCP get_query_results description is updated.
• The frontend EnvAccess type gains query_tier, carve_tier and scope_tags.
• The users dialog no longer sends tiers back, so unchecking query or carve still revokes it.
In this PR standard and full reach the same nodes. Part 3 adds sensitive nodes, which standard will exclude inside NodeScope.
Security and operational impact
• Everything is enforced in osctrl-api. osctrl-tls is not changed.
• Upgrade grants nobody new access. Node detail, posture and the unpaged node lists stay closed to user-level-only operators, as before.
• Permission lookups (tiers, scope tags) use the per-request cache. List handlers make one filter query, not one per node.
• API contract: a permissions body that sets query_tier or carve_tier must also set `query`/`carve` to true, or it means none.
• CLI permission output now includes query_tier and carve_tier.
Known limits
• Tiers are API-only for now. The CLI and pkg/apiclient still send only booleans, which map to standard. The UI editor for tiers and scope tags comes in part 5.
• Revoking a grant does not cancel pending work. Results are still hidden at read time.
• No untag endpoint exists. If one is added, it must call cancelOutOfScope.
Validation
• go build ./... and go test ./... pass.
• New tests cover:
• the tier model and the legacy boolean round trip;
• tag grants and the scope filter, including empty and deleted tags;
• targeting, including cross-environment and missing vs out-of-scope nodes;
• node, result and carve visibility (DB and S3 readers, CSV);
• cancellation on tag delete;
• alert-admin rule tiers.
• make openapi-check passes.
• cd frontend && npm run check passes.
• `golangci-lint`: no new findings.
• git diff --stat -- cmd/tls is empty.
• Not run: Playwright and in-browser checks. The only frontend change is the users dialog payload, with no visual change, so there are no screenshots.
jmpsec/osctrlGitHub
11/07/2026, 9:11 PM