diff --git a/client/cmd/service.go b/client/cmd/service.go index 7d477ec79..2a2111c98 100644 --- a/client/cmd/service.go +++ b/client/cmd/service.go @@ -46,7 +46,7 @@ type program struct { jsonServMu sync.Mutex serverInstance *server.Server serverInstanceMu sync.Mutex - policyGate *ipcauth.PolicyGate + ruleGate *ipcauth.RuleGate } func init() { diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index 1feed57bb..3a06ea7ff 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -80,12 +80,12 @@ func (p *program) Start(svc service.Service) error { return fmt.Errorf("parse daemon address: %w", err) } - p.policyGate = ipcauth.NewPolicyGate() + p.ruleGate = ipcauth.NewRuleGate() // in any case, even if configuration does not exists we run daemon to serve CLI gRPC API. opts := append(daemonServerOptions(network), - grpc.ChainUnaryInterceptor(p.policyGate.UnaryPolicyInterceptor()), - grpc.ChainStreamInterceptor(p.policyGate.StreamPolicyInterceptor()), + grpc.ChainUnaryInterceptor(p.ruleGate.UnaryPolicyInterceptor()), + grpc.ChainStreamInterceptor(p.ruleGate.StreamPolicyInterceptor()), ) p.serv = grpc.NewServer(opts...) @@ -151,7 +151,7 @@ func (p *program) serve(daemonListener, jsonListener *socketListener) error { } serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, captureEnabled, networksDisabled) - p.policyGate.SetPolicy(serverInstance) + p.ruleGate.SetState(serverInstance) if err := serverInstance.Start(); err != nil { return fmt.Errorf("start daemon: %w", err) } diff --git a/client/internal/ipcauth/policy.go b/client/internal/ipcauth/policy.go index 2562aa100..c1deee937 100644 --- a/client/internal/ipcauth/policy.go +++ b/client/internal/ipcauth/policy.go @@ -10,70 +10,78 @@ import ( "google.golang.org/grpc/status" ) -type Policy interface { +type DaemonState interface { SessionHolder() (Identity, bool) } -type PolicyGate struct { - mu sync.Mutex - policy Policy +type Rule func(id Identity, st DaemonState) error + +type RuleGate struct { + mu sync.Mutex + rules []Rule + st DaemonState } -func NewPolicyGate() *PolicyGate { - return &PolicyGate{} +func NewRuleGate() *RuleGate { + return &RuleGate{} } -func (g *PolicyGate) SetPolicy(p Policy) { +func (g *RuleGate) SetState(st DaemonState) { g.mu.Lock() defer g.mu.Unlock() - g.policy = p + g.st = st } -func (g *PolicyGate) SessionHolder() (Identity, bool) { +func (g *RuleGate) SetRule(r Rule) { g.mu.Lock() defer g.mu.Unlock() - if g.policy == nil { - return Identity{}, false + g.rules = append(g.rules, r) +} + +func (g *RuleGate) state() DaemonState { + g.mu.Lock() + defer g.mu.Unlock() + return g.st +} + +func RequireSessionHolder(id Identity, st DaemonState) error { + holder, running := st.SessionHolder() + log.Debugf("id : %v, session holder: %v", id, holder) + if !running || holder.SameUser(id) || holder.IsPrivileged() { + return nil } - return g.policy.SessionHolder() + return status.Errorf(codes.PermissionDenied, "session is held by another user (%v)", holder) } -func (g *PolicyGate) StreamPolicyInterceptor() grpc.StreamServerInterceptor { +func (g *RuleGate) StreamPolicyInterceptor() grpc.StreamServerInterceptor { return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - if !g.authorize(ss.Context()) { - return status.Error(codes.PermissionDenied, "caller is not session owner") + if authErr := g.authorize(ss.Context()); authErr != nil { + return authErr } return handler(ss.Context(), ss) } } -func (g *PolicyGate) UnaryPolicyInterceptor() grpc.UnaryServerInterceptor { +func (g *RuleGate) UnaryPolicyInterceptor() grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { - if !g.authorize(ctx) { - return nil, status.Error(codes.PermissionDenied, "caller is not session owner") + if authErr := g.authorize(ctx); authErr != nil { + return nil, authErr } return handler(ctx, req) } } -func (g *PolicyGate) authorize(ctx context.Context) bool { +func (g *RuleGate) authorize(ctx context.Context) error { id, ok := CallerIdentity(ctx) if !ok { - return false + return status.Error(codes.PermissionDenied, "caller cannot be verified") } - g.mu.Lock() - if g.policy == nil { - g.mu.Unlock() - return false + state := g.state() + for _, rule := range g.rules { + ruleErr := rule(id, state) + if ruleErr != nil { + return ruleErr + } } - sessionId, running := g.policy.SessionHolder() - // TODO improve logging - log.Infof("id : %v, session holder: %v", id, sessionId) - g.mu.Unlock() - // TODO windows - // TODO allow root - if running && sessionId.UID != id.UID { - return false - } - return true + return nil } diff --git a/client/server/server.go b/client/server/server.go index 07b3c45bc..ea9a6e646 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -35,7 +35,6 @@ import ( "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/client/internal" - "github.com/netbirdio/netbird/client/internal/ipcauth" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/client/internal/updater"