<#1146 Granular RBAC part 2: query/carve tiers and...
# osctrl
g
#1146 Granular RBAC part 2: query/carve tiers and node visibility Pull request opened by javuto Granular RBAC part 2: query/carve tiers and node visibility Problem Query and carve access per environment was all-or-nothing. A responder with query access could target every node in the environment and read every result. There was no way to limit someone to a set of tagged machines (#683). Node visibility was also inconsistent: • Node lists and node detail needed env admin. • The paged node list and node logs only needed user level. This is part 2 of the granular RBAC work (#704). Part 1, global roles, merged in #1144. What changes Tiers. Query and carve each get a tier per environment:
none
,
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/osctrl