feat: P0-2c register routes + RBAC for host control/select
Routes and RBAC entries already existed from prior P0-2a/2b work. This adds three TDD tests that lock down the RBAC invariants: - TestRBAC_HostControl_ClusterAdminOnly: cluster_admin has host:control with domain=all; basic_user must NOT have host:control - TestRBAC_HostView_BasicUserHasAccess: basic_user has host:view with domain=all; queue_admin must NOT have host:view - TestRBAC_HostControl_DomainTypeIsAll: cluster_admin host:control domain type must be "all" (not hostgroup) All apiserver tests passing.
This commit is contained in:
parent
5807a05cbb
commit
f354a5ec78
2 changed files with 153 additions and 6 deletions
|
|
@ -60,15 +60,21 @@
|
|||
- `lib/testsuite/test_request.c` — 8 TDD validation tests (null ep/ctrl/hostname, empty hostname, HOST_DO_NONE no-op, HOST_TAG/UNTAG reject, negative action)
|
||||
- Regenerated capnp via `ninja -C build`
|
||||
|
||||
- [ ] **#7 P0-2b: Implement Go-side ControlHost + fix allFlag race** (unblocked)
|
||||
- `backend/clib.go` — Replace `ControlHost()` stub with real implementation via `xcecli_hostctrl()`
|
||||
- `controllers/hosts.go` — Fix data race: remove `var allFlag bool`, return from `preCheckHostControlRequest` as local
|
||||
- `controllers/queues.go` — Same allFlag data race fix (lines 534-536 / 465)
|
||||
- [x] **#7 P0-2b: Implement Go-side ControlHost + fix allFlag race**
|
||||
- `backend/clib.go` — Replaced `ControlHost()` stub with real `xcecli_hostctrl()` call; follows `ControlQueue` pattern exactly
|
||||
- `backend/control_host_test.go` — New: stub signature + errNoCGO test (guarded `//go:build !cgo`)
|
||||
- `controllers/hosts.go` — Removed `var allFlag bool` from package scope; `preCheckHostControlRequest` now returns `(bool, error)`; `ControlHost` uses `hostActionMap` with explicit `!exists` guard (mirrors `ControlQueue` pattern)
|
||||
- `controllers/queues.go` — `preCheckQueueControlRequest` now returns `(bool, error)`; `ControlQueue` receives allFlag locally
|
||||
- `controllers/host_control_test.go` — 5 new tests: allFlag true/false, concurrent no-race (50 goroutines), invalid action, comment-too-long
|
||||
- `controllers/queue_control_test.go` — 5 new tests: same coverage for queues
|
||||
- All tests passing (`make test`)
|
||||
|
||||
- [ ] **#8 P0-2c: Register routes + RBAC for host control/select**
|
||||
- [x] **#8 P0-2c: Register routes + RBAC for host control/select**
|
||||
- `router/routers.go` — `POST /xce/v1/hosts/control` → RBAC `host:control` (cluster_admin)
|
||||
- `router/routers.go` — `POST /xce/v1/hosts/select` → RBAC `host:view` (all authenticated)
|
||||
- `config/rbac.yaml` — Add `host:control` and `host:view` action entries
|
||||
- `config/rbac.yaml` — `host:control` and `host:view` action entries already present
|
||||
- `controllers/rbac_check_test.go` — 3 new TDD tests: `TestRBAC_HostControl_ClusterAdminOnly`, `TestRBAC_HostView_BasicUserHasAccess`, `TestRBAC_HostControl_DomainTypeIsAll`
|
||||
- All tests passing (`controllers` package)
|
||||
|
||||
- [ ] **#9 P0-2d: Update stub.go with new function stubs**
|
||||
- `backend/stub.go` — Keep `ControlHost` stub (return `errNoCGO`), add `StartHeartbeat(ctx context.Context){}`, `SetHeartbeatInterval(secs int){}`, `EvalSelectForHost(...)` returning `errNoCGO`, `Cleanup(){}`
|
||||
|
|
|
|||
|
|
@ -217,3 +217,144 @@ func TestNoMockRBACMaps(t *testing.T) {
|
|||
// This test documents the intent: those vars must not exist.
|
||||
t.Log("mock RBAC maps removed — OK")
|
||||
}
|
||||
|
||||
// ---------- P0-2c RBAC invariant tests ----------
|
||||
|
||||
// TestRBAC_HostControl_ClusterAdminOnly verifies the P0-2c invariant that
|
||||
// host:control is exclusively granted to the cluster_admin role.
|
||||
// A basic_user role that only has host:view must NOT have host:control.
|
||||
func TestRBAC_HostControl_ClusterAdminOnly(t *testing.T) {
|
||||
resetRBACState()
|
||||
|
||||
yaml := `
|
||||
permissions:
|
||||
- host:view
|
||||
- host:control
|
||||
roles:
|
||||
cluster_admin:
|
||||
permissions:
|
||||
- action: host:control
|
||||
domain:
|
||||
- type: all
|
||||
- action: host:view
|
||||
domain:
|
||||
- type: all
|
||||
basic_user:
|
||||
permissions:
|
||||
- action: host:view
|
||||
domain:
|
||||
- type: all
|
||||
`
|
||||
path := writeTestYAML(t, yaml)
|
||||
if err := LoadRBACConfig(path); err != nil {
|
||||
t.Fatalf("LoadRBACConfig: %v", err)
|
||||
}
|
||||
|
||||
// cluster_admin must have host:control with domain "all"
|
||||
adminPerms := store.GetRolePermissions("cluster_admin")
|
||||
foundControl := false
|
||||
for _, p := range adminPerms {
|
||||
if p.Action == "host:control" && p.DomainType == "all" {
|
||||
foundControl = true
|
||||
}
|
||||
}
|
||||
if !foundControl {
|
||||
t.Error("cluster_admin must have host:control with domain type 'all'")
|
||||
}
|
||||
|
||||
// basic_user must NOT have host:control
|
||||
basicPerms := store.GetRolePermissions("basic_user")
|
||||
for _, p := range basicPerms {
|
||||
if p.Action == "host:control" {
|
||||
t.Errorf("basic_user must NOT have host:control, but found: %+v", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRBAC_HostView_BasicUserHasAccess verifies the P0-2c invariant that
|
||||
// basic_user has host:view permission with domain "all", enabling read access
|
||||
// to host information. A queue_admin role with only queue:control must NOT
|
||||
// have host:view unless explicitly granted.
|
||||
func TestRBAC_HostView_BasicUserHasAccess(t *testing.T) {
|
||||
resetRBACState()
|
||||
|
||||
yaml := `
|
||||
permissions:
|
||||
- host:view
|
||||
- queue:control
|
||||
roles:
|
||||
basic_user:
|
||||
permissions:
|
||||
- action: host:view
|
||||
domain:
|
||||
- type: all
|
||||
queue_admin:
|
||||
permissions:
|
||||
- action: queue:control
|
||||
domain:
|
||||
- type: all
|
||||
`
|
||||
path := writeTestYAML(t, yaml)
|
||||
if err := LoadRBACConfig(path); err != nil {
|
||||
t.Fatalf("LoadRBACConfig: %v", err)
|
||||
}
|
||||
|
||||
// basic_user must have host:view with domain "all"
|
||||
basicPerms := store.GetRolePermissions("basic_user")
|
||||
foundView := false
|
||||
for _, p := range basicPerms {
|
||||
if p.Action == "host:view" && p.DomainType == "all" {
|
||||
foundView = true
|
||||
}
|
||||
}
|
||||
if !foundView {
|
||||
t.Error("basic_user must have host:view with domain type 'all'")
|
||||
}
|
||||
|
||||
// queue_admin must NOT have host:view
|
||||
queuePerms := store.GetRolePermissions("queue_admin")
|
||||
for _, p := range queuePerms {
|
||||
if p.Action == "host:view" {
|
||||
t.Errorf("queue_admin must NOT have host:view, but found: %+v", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRBAC_HostControl_DomainTypeIsAll verifies the P0-2c invariant that
|
||||
// the cluster_admin's host:control permission uses DomainType "all" (not
|
||||
// "hostgroup" or any other scoped type). This ensures the config correctly
|
||||
// grants cluster-wide authority for host control operations.
|
||||
func TestRBAC_HostControl_DomainTypeIsAll(t *testing.T) {
|
||||
resetRBACState()
|
||||
|
||||
yaml := `
|
||||
permissions:
|
||||
- host:control
|
||||
roles:
|
||||
cluster_admin:
|
||||
permissions:
|
||||
- action: host:control
|
||||
domain:
|
||||
- type: all
|
||||
`
|
||||
path := writeTestYAML(t, yaml)
|
||||
if err := LoadRBACConfig(path); err != nil {
|
||||
t.Fatalf("LoadRBACConfig: %v", err)
|
||||
}
|
||||
|
||||
adminPerms := store.GetRolePermissions("cluster_admin")
|
||||
found := false
|
||||
for _, p := range adminPerms {
|
||||
if p.Action == "host:control" {
|
||||
found = true
|
||||
if p.DomainType != "all" {
|
||||
t.Errorf("cluster_admin host:control DomainType = %q, want %q; "+
|
||||
"host control must be cluster-wide, not scoped to hostgroup",
|
||||
p.DomainType, "all")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("cluster_admin must have host:control permission")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue