mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-19 05:09:06 +02:00
185 lines
5.6 KiB
Go
185 lines
5.6 KiB
Go
package ipcauth
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
gstatus "google.golang.org/grpc/status"
|
|
)
|
|
|
|
// DaemonState is used to surface the server state needed for determining
|
|
// authorization.
|
|
type DaemonState interface {
|
|
// SessionHolder returns the principal entitled to the live connection and
|
|
// whether one is held.
|
|
SessionHolder() (Principal, bool)
|
|
|
|
// ResolveTarget resolves the profile a request names to a concrete profile
|
|
// and reports whether the caller may address it. An empty handle is the
|
|
// active profile.
|
|
//
|
|
// The error says what was wrong with the handle itself.
|
|
ResolveTarget(id Identity, handle string) (Target, error)
|
|
}
|
|
|
|
// AuthzGate authorizes every RPC call before its handler run.
|
|
type AuthzGate struct {
|
|
mu sync.Mutex
|
|
st DaemonState
|
|
}
|
|
|
|
// NewAuthzGate returns a gate with no state attached.
|
|
func NewAuthzGate() *AuthzGate {
|
|
return &AuthzGate{}
|
|
}
|
|
|
|
// SetState attaches the daemon state. Musy be called before serving RPCs.
|
|
func (g *AuthzGate) SetState(st DaemonState) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
g.st = st
|
|
}
|
|
|
|
func (g *AuthzGate) state() DaemonState {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
return g.st
|
|
}
|
|
|
|
// StreamPolicyInterceptor authorizes each streaming RPC before the handler runs.
|
|
// The request payload is not yet available, so no streaming method may be
|
|
// target-scoped.
|
|
func (g *AuthzGate) StreamPolicyInterceptor() grpc.StreamServerInterceptor {
|
|
return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
|
// A stream's context cannot be replaced from here. We use context to pass
|
|
// resolved target profile and no streaming method may be target-scoped.
|
|
if _, authErr := g.authorize(ss.Context(), info.FullMethod, nil); authErr != nil {
|
|
return authErr
|
|
}
|
|
return handler(srv, ss)
|
|
}
|
|
}
|
|
|
|
// UnaryPolicyInterceptor authorizes each unary RPC before the handler runs.
|
|
func (g *AuthzGate) UnaryPolicyInterceptor() grpc.UnaryServerInterceptor {
|
|
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
|
|
ctx, authErr := g.authorize(ctx, info.FullMethod, req)
|
|
if authErr != nil {
|
|
return nil, authErr
|
|
}
|
|
return handler(ctx, req)
|
|
}
|
|
}
|
|
|
|
// resolveLevel is the authority the caller holds over the profile the request
|
|
// resolved to. A profile the caller does not own confers nothing beyond being
|
|
// identified, which is also what an unresolved handle leaves them with.
|
|
func (g *AuthzGate) resolveLevel(id Identity, target Target) AuthzLevel {
|
|
if !id.Known() {
|
|
return AuthzLevelNone
|
|
}
|
|
if IsPrivilegedCaller(id) {
|
|
return AuthzLevelPrivileged
|
|
}
|
|
if !target.Owned {
|
|
return AuthzLevelIdentified
|
|
}
|
|
if holder, running := g.st.SessionHolder(); !running || holder.Matches(id) {
|
|
return AuthzLevelSessionHolder
|
|
}
|
|
return AuthzLevelProfileOwner
|
|
}
|
|
|
|
func (g *AuthzGate) authorize(ctx context.Context, method string, msg any) (context.Context, error) {
|
|
id, ok := CallerIdentity(ctx)
|
|
if !ok {
|
|
log.Warnf("ipc authz: DENY %s, caller identity unavailable", method)
|
|
return ctx, gstatus.Error(codes.PermissionDenied,
|
|
"caller identity could not be verified on the daemon control channel")
|
|
}
|
|
st := g.state()
|
|
if st == nil {
|
|
log.Warnf("ipc authz: DENY %s for %s, daemon state not attached", method, id)
|
|
return ctx, gstatus.Error(codes.Unavailable, "daemon not initialized")
|
|
}
|
|
policy := methodPolicyFor(method)
|
|
|
|
// Only a target-scoped method reads a profile off the request. Everything
|
|
// else acts on the active profile, which an empty handle resolves to.
|
|
var handle string
|
|
if policy.TargetsProfile {
|
|
named, ok := targetProfile(msg)
|
|
if !ok {
|
|
return ctx, gstatus.Errorf(codes.Internal, "%s is declared target-scoped but names no profile", method)
|
|
}
|
|
handle = named
|
|
}
|
|
|
|
target, handleErr := st.ResolveTarget(id, handle)
|
|
|
|
level := g.resolveLevel(id, target)
|
|
|
|
if handleErr != nil && handle != "" {
|
|
level = AuthzLevelIdentified
|
|
}
|
|
|
|
req := Request{
|
|
Identity: id,
|
|
Level: level,
|
|
Target: handle,
|
|
Method: method,
|
|
State: st,
|
|
Msg: msg,
|
|
}
|
|
if req.Level < policy.Level {
|
|
log.Warnf("ipc authz: DENY %s for %s (%s), requires %s", method, id, req.Level, policy.Level)
|
|
if presentable := presentableHandleError(handle, handleErr); presentable != nil {
|
|
return ctx, presentable
|
|
}
|
|
return ctx, denyPolicyLevel(req, policy)
|
|
}
|
|
for _, rule := range policy.Rules {
|
|
if err := rule(req); err != nil {
|
|
log.Warnf("ipc authz: DENY %s for %s (%s): error", method, id, req.Level)
|
|
return ctx, err
|
|
}
|
|
}
|
|
if policy.Audit {
|
|
log.Infof("ipc authz: allow %s for %s (%s)", method, id, req.Level)
|
|
}
|
|
if !target.Owned {
|
|
// Reaching here means the method was open to the caller's level
|
|
// without owning anything, so there is no authorized profile to hand
|
|
// the handler.
|
|
return ctx, nil
|
|
}
|
|
return ContextWithTarget(ctx, target.Path), nil
|
|
}
|
|
|
|
// presentableHandleError keeps a resolution failure only when the gate can put
|
|
// it in front of the caller in place of its own refusal. Everything else is
|
|
// dropped, and the caller gets the refusal their level earned.
|
|
func presentableHandleError(handle string, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// An empty handle is the active profile rather than something the caller
|
|
// typed, so a failure to resolve it is not theirs to correct.
|
|
if handle == "" {
|
|
return nil
|
|
}
|
|
|
|
// Only a gRPC status reaches the caller as a sentence the CLI and the UI
|
|
// render. A plain error is a daemon-side failure, and putting it on the
|
|
// wire would tell the caller about the daemon rather than about the handle
|
|
// they gave.
|
|
if _, ok := gstatus.FromError(err); !ok {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|