mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-18 04:29:54 +00:00
171 lines
5.7 KiB
Go
171 lines
5.7 KiB
Go
package owner
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"sync"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/owner/consoleuser"
|
|
)
|
|
|
|
const servicePath = "/daemon.DaemonService/"
|
|
|
|
// profileBypassMethods skip the active-profile owner check. They either
|
|
// operate on a specific target profile (and the handler enforces target-profile
|
|
// owner-or-root itself) or are per-user listings/creations that don't affect
|
|
// the active session and shouldn't require active-profile ownership. Peer
|
|
// credentials are still required.
|
|
var profileBypassMethods = map[string]bool{
|
|
servicePath + "AddProfile": true,
|
|
servicePath + "ListProfiles": true,
|
|
servicePath + "RemoveProfile": true,
|
|
servicePath + "SwitchProfile": true,
|
|
}
|
|
|
|
// Error messages returned to denied callers. They are multi-line so the
|
|
// suggested commands sit on their own line for easy triple-click copy-paste.
|
|
const (
|
|
errNoPeerCreds = "peer credentials unavailable; rerun via the netbird CLI"
|
|
|
|
errNoOwnerConfigured = `no daemon owner is configured and no console-session user matches your UID.
|
|
Run as root for one-off use:
|
|
sudo netbird ...
|
|
Or call from the active console session: the first call from the user logged in
|
|
at the GUI/console claims ownership automatically.`
|
|
|
|
errOwnerRequired = `this operation requires root or the daemon owner (uid %d is not an owner).
|
|
Run as root for one-off use:
|
|
sudo netbird ...
|
|
Or ask an existing owner (or root) to add you:
|
|
sudo netbird owner add %[1]d`
|
|
)
|
|
|
|
// consoleUIDLookup is the function used to look up the active console UID.
|
|
// Overridable in tests; defaults to the platform implementation.
|
|
var consoleUIDLookup = consoleuser.ActiveUID
|
|
|
|
// OwnerConfig provides access to the current owner UIDs setting.
|
|
// The interceptor reads and writes through this interface so it can
|
|
// work with the profile manager's config without a direct dependency.
|
|
type OwnerConfig interface {
|
|
// GetOwnerUIDs returns the current owner UIDs.
|
|
// nil means legacy/migration TOFU (field absent from existing config).
|
|
// empty means fresh install (root-only with console-user TOFU exception).
|
|
// populated means those UIDs plus root may control the daemon.
|
|
GetOwnerUIDs() []UID
|
|
|
|
// AddOwnerUID adds the given UID to the owner list and persists it.
|
|
AddOwnerUID(uid UID) error
|
|
}
|
|
|
|
// Interceptor enforces owner restrictions on the daemon gRPC socket.
|
|
type Interceptor struct {
|
|
config OwnerConfig
|
|
// mu serializes the read-then-write of OwnerUIDs during TOFU/claim flows
|
|
// so two concurrent first-callers can't both end up persisted as owners.
|
|
// Holds across the OwnerConfig.AddOwnerUID call; safe because no callback
|
|
// path takes this mutex.
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewInterceptor creates an owner interceptor backed by the given config.
|
|
func NewInterceptor(config OwnerConfig) *Interceptor {
|
|
return &Interceptor{config: config}
|
|
}
|
|
|
|
// UnaryInterceptor returns a gRPC unary server interceptor that enforces owner policy.
|
|
func (i *Interceptor) UnaryInterceptor() grpc.UnaryServerInterceptor {
|
|
return func(
|
|
ctx context.Context,
|
|
req any,
|
|
info *grpc.UnaryServerInfo,
|
|
handler grpc.UnaryHandler,
|
|
) (any, error) {
|
|
if err := i.authorize(ctx, info.FullMethod); err != nil {
|
|
return nil, err
|
|
}
|
|
return handler(ctx, req)
|
|
}
|
|
}
|
|
|
|
// StreamInterceptor returns a gRPC stream server interceptor that enforces owner policy.
|
|
func (i *Interceptor) StreamInterceptor() grpc.StreamServerInterceptor {
|
|
return func(
|
|
srv any,
|
|
ss grpc.ServerStream,
|
|
info *grpc.StreamServerInfo,
|
|
handler grpc.StreamHandler,
|
|
) error {
|
|
if err := i.authorize(ss.Context(), info.FullMethod); err != nil {
|
|
return err
|
|
}
|
|
return handler(srv, ss)
|
|
}
|
|
}
|
|
|
|
// authorize checks whether the caller is allowed to call the given method.
|
|
// Every RPC is gated; root is always allowed. Non-root callers are accepted
|
|
// when they are existing owners, when the config is in legacy TOFU state
|
|
// (claim on first call, preserves pre-enforcement behavior), or when the
|
|
// config is in fresh-install state and they match the active console user.
|
|
func (i *Interceptor) authorize(ctx context.Context, fullMethod string) error {
|
|
uid, ok := UIDFromContext(ctx)
|
|
if !ok {
|
|
return status.Error(codes.PermissionDenied, errNoPeerCreds)
|
|
}
|
|
|
|
if uid == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Profile-management RPCs do their own per-target authorization in the
|
|
// handler. The interceptor only confirms peer credentials are present.
|
|
if profileBypassMethods[fullMethod] {
|
|
return nil
|
|
}
|
|
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
|
|
ownerUIDs := i.config.GetOwnerUIDs()
|
|
|
|
switch {
|
|
case ownerUIDs == nil:
|
|
// Legacy / migration TOFU: existing pre-enforcement config has no
|
|
// owners field. Any non-root local caller claims on first call so
|
|
// upgrades don't break.
|
|
return i.claim(uid, "migration TOFU")
|
|
|
|
case len(ownerUIDs) == 0:
|
|
// Fresh-install root-only mode with a console-user exception so the
|
|
// GUI/CLI just works for the user physically at the machine. SSH'd
|
|
// or otherwise non-console callers are denied.
|
|
consoleUID, ok := consoleUIDLookup()
|
|
if ok && uint32(uid) == consoleUID {
|
|
return i.claim(uid, "console-user TOFU")
|
|
}
|
|
return status.Error(codes.PermissionDenied, errNoOwnerConfigured)
|
|
|
|
case slices.Contains(ownerUIDs, uid):
|
|
return nil
|
|
|
|
default:
|
|
return status.Errorf(codes.PermissionDenied, errOwnerRequired, uid)
|
|
}
|
|
}
|
|
|
|
// claim adds uid to the owner list and persists it. The caller must hold i.mu.
|
|
func (i *Interceptor) claim(uid UID, reason string) error {
|
|
log.Infof("%s: claiming owner for UID %d", reason, uid)
|
|
if err := i.config.AddOwnerUID(uid); err != nil {
|
|
log.Errorf("persist owner UID: %v", err)
|
|
return status.Error(codes.Internal, "persist owner UID")
|
|
}
|
|
return nil
|
|
}
|