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

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] {
i.auditAllow(id, fullMethod)
return nil
}

View File

@@ -42,9 +42,11 @@ func ctxWith(id Identity) context.Context {
}
const (
up = servicePath + "Up"
list = servicePath + "ListProfiles"
unkwn = servicePath + "SomeFutureMethod"
up = servicePath + "Up"
list = servicePath + "ListProfiles"
unkwn = servicePath + "SomeFutureMethod"
down = servicePath + "Down"
switchp = servicePath + "SwitchProfile"
)
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},
{"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},
{"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},
{"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"}},

View File

@@ -18,15 +18,26 @@ type ProfilePolicy interface {
ClaimActiveProfileOwnerIfUnowned(id Identity) (bool, error)
}
// handlerAuthorizedMethods bypass the active-profile gate: they are per-user or
// per-target-profile operations whose handler does its own authorization (bound
// to the caller identity). Peer identity is still required to reach them.
// handlerAuthorizedMethods bypass the active-profile gate. Peer identity is
// still required to reach them. Two groups:
//
// - 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{
servicePath + "AddProfile": true,
servicePath + "ListProfiles": true,
servicePath + "GetActiveProfile": true,
servicePath + "RemoveProfile": true,
servicePath + "RenameProfile": true,
servicePath + "SwitchProfile": true,
servicePath + "Down": true,
}
// auditMethods are worth an audit log line. Denials are always logged.