Relax profile command ACLs

This commit is contained in:
Theodor S. Midtlien
2026-07-25 16:09:22 +02:00
parent cd98648a67
commit ae9ee15501
3 changed files with 25 additions and 7 deletions
+4 -1
View File
@@ -72,8 +72,11 @@ func (i *Interceptor) authorize(ctx context.Context, fullMethod string) error {
} }
} }
// Per-user / per-target-profile RPCs authorize themselves in the handler. // These RPCs authorize themselves in the handler (target-profile check) or are
// connection-lifecycle actions any authenticated local user may perform; they
// bypass the active-profile gate. Still audit the C/H ones on allow.
if handlerAuthorizedMethods[fullMethod] { if handlerAuthorizedMethods[fullMethod] {
i.auditAllow(id, fullMethod)
return nil return nil
} }
+7 -3
View File
@@ -42,9 +42,11 @@ func ctxWith(id Identity) context.Context {
} }
const ( const (
up = servicePath + "Up" up = servicePath + "Up"
list = servicePath + "ListProfiles" list = servicePath + "ListProfiles"
unkwn = servicePath + "SomeFutureMethod" unkwn = servicePath + "SomeFutureMethod"
down = servicePath + "Down"
switchp = servicePath + "SwitchProfile"
) )
func TestInterceptorAuthorize(t *testing.T) { func TestInterceptorAuthorize(t *testing.T) {
@@ -65,6 +67,8 @@ func TestInterceptorAuthorize(t *testing.T) {
{"uid owner allowed", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 1000}), up, false}, {"uid owner allowed", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 1000}), up, false},
{"non-owner denied", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 2000}), up, true}, {"non-owner denied", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 2000}), up, true},
{"handler-authorized bypass", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 2000}), list, false}, {"handler-authorized bypass", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 2000}), list, false},
{"down allowed while another user's profile active", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 2000}), down, false},
{"switch-profile bypasses active-profile gate", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 2000}), switchp, false},
{"unknown method gated", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 2000}), unkwn, true}, {"unknown method gated", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 2000}), unkwn, true},
{"primary gid owner", Ownership{Owners: []string{"gid:5000"}}, nil, ctxWith(Identity{UID: 2000, GID: 5000}), up, false}, {"primary gid owner", Ownership{Owners: []string{"gid:5000"}}, nil, ctxWith(Identity{UID: 2000, GID: 5000}), up, false},
{"group-name owner via resolver", Ownership{Owners: []string{"group:admins"}}, {"group-name owner via resolver", Ownership{Owners: []string{"group:admins"}},
+14 -3
View File
@@ -18,15 +18,26 @@ type ProfilePolicy interface {
ClaimActiveProfileOwnerIfUnowned(id Identity) (bool, error) ClaimActiveProfileOwnerIfUnowned(id Identity) (bool, error)
} }
// handlerAuthorizedMethods bypass the active-profile gate: they are per-user or // handlerAuthorizedMethods bypass the active-profile gate. Peer identity is
// per-target-profile operations whose handler does its own authorization (bound // still required to reach them. Two groups:
// to the caller identity). Peer identity is still required to reach them. //
// - Per-user / per-target-profile operations whose handler does its own
// authorization bound to the caller identity: AddProfile, ListProfiles,
// GetActiveProfile, RemoveProfile, RenameProfile, and SwitchProfile (gated
// on the TARGET profile's ownership via authorizeTargetProfile).
// - Connection-lifecycle operations on the single shared daemon connection
// that any authenticated local user may perform: Down. Gating these on the
// ACTIVE profile's owner would trap a user behind another user's profile —
// they could neither disconnect nor switch to their OWN profile. SwitchProfile
// is bounded by its target check; Down only tears the connection down.
var handlerAuthorizedMethods = map[string]bool{ var handlerAuthorizedMethods = map[string]bool{
servicePath + "AddProfile": true, servicePath + "AddProfile": true,
servicePath + "ListProfiles": true, servicePath + "ListProfiles": true,
servicePath + "GetActiveProfile": true, servicePath + "GetActiveProfile": true,
servicePath + "RemoveProfile": true, servicePath + "RemoveProfile": true,
servicePath + "RenameProfile": true, servicePath + "RenameProfile": true,
servicePath + "SwitchProfile": true,
servicePath + "Down": true,
} }
// auditMethods are worth an audit log line. Denials are always logged. // auditMethods are worth an audit log line. Denials are always logged.