mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-29 07:49:14 +01:00
Small refactor of Authz
This commit is contained in:
@@ -4,11 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
gstatus "google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DaemonState is used to surface the server state needed for determining
|
// DaemonState is used to surface the server state needed for determining
|
||||||
@@ -50,70 +49,6 @@ func (g *AuthzGate) state() DaemonState {
|
|||||||
return g.st
|
return g.st
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequireHolderForFullStatus escalates a StatusRequest that asks for peer detail
|
|
||||||
// or for probes to be run.
|
|
||||||
func RequireHolderForFullStatus(r Request) error {
|
|
||||||
statusReq, ok := r.Msg.(*proto.StatusRequest)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if r.Level < AuthzLevelSessionHolder {
|
|
||||||
if statusReq.GetFullPeerStatus {
|
|
||||||
statusReq.GetFullPeerStatus = false
|
|
||||||
}
|
|
||||||
if statusReq.ShouldRunProbes {
|
|
||||||
statusReq.ShouldRunProbes = false
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return RequireLevel(AuthzLevelSessionHolder)(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequireLevel builds a rule from a level, for composing inside another rule.
|
|
||||||
func RequireLevel(want AuthzLevel) Rule {
|
|
||||||
return func(r Request) error {
|
|
||||||
if r.Level >= want {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return denyLevel(r, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func denyLevel(r Request, want AuthzLevel) error {
|
|
||||||
return status.Errorf(codes.PermissionDenied,
|
|
||||||
"%s requires %s, caller %s is %s", r.Method, want, r.Identity, r.Level)
|
|
||||||
}
|
|
||||||
|
|
||||||
// denyPolicyLevel refuses a caller at the gate, where the policy is in hand.
|
|
||||||
//
|
|
||||||
// Requiring privilege is the one denial a caller can act on, so it carries the
|
|
||||||
// elevated command rather than a bare refusal. A privileged method that declares
|
|
||||||
// no action keeps the plain message. Rules deny through denyLevel instead: they
|
|
||||||
// cannot reach the policy table without an initialization cycle, and no rule
|
|
||||||
// requires privilege.
|
|
||||||
func denyPolicyLevel(r Request, p MethodPolicy) error {
|
|
||||||
switch p.Level {
|
|
||||||
case AuthzLevelPrivileged:
|
|
||||||
if p.Action != "" {
|
|
||||||
actor, command := RequiredActor(p.Command)
|
|
||||||
return PrivilegeError(PrivilegeSummary(p.Action, actor), command)
|
|
||||||
}
|
|
||||||
|
|
||||||
case AuthzLevelSessionHolder:
|
|
||||||
// resolveLevel stops at profile owner only when a session is running and
|
|
||||||
// somebody else holds it.
|
|
||||||
if r.Level == AuthzLevelProfileOwner {
|
|
||||||
return SessionHeldError(p.Action)
|
|
||||||
}
|
|
||||||
return NotOwnerError(p.Action)
|
|
||||||
|
|
||||||
case AuthzLevelProfileOwner:
|
|
||||||
return NotOwnerError(p.Action)
|
|
||||||
}
|
|
||||||
|
|
||||||
return denyLevel(r, p.Level)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StreamPolicyInterceptor authorizes each streaming RPC before the handler runs.
|
// StreamPolicyInterceptor authorizes each streaming RPC before the handler runs.
|
||||||
// The request payload is not yet available, so no streaming method may be
|
// The request payload is not yet available, so no streaming method may be
|
||||||
// target-scoped.
|
// target-scoped.
|
||||||
@@ -139,17 +74,36 @@ func (g *AuthzGate) UnaryPolicyInterceptor() grpc.UnaryServerInterceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func (g *AuthzGate) authorize(ctx context.Context, method string, msg any) (context.Context, error) {
|
||||||
id, ok := CallerIdentity(ctx)
|
id, ok := CallerIdentity(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Warnf("ipc authz: DENY %s, caller identity unavailable", method)
|
log.Warnf("ipc authz: DENY %s, caller identity unavailable", method)
|
||||||
return ctx, status.Error(codes.PermissionDenied,
|
return ctx, gstatus.Error(codes.PermissionDenied,
|
||||||
"caller identity could not be verified on the daemon control channel")
|
"caller identity could not be verified on the daemon control channel")
|
||||||
}
|
}
|
||||||
st := g.state()
|
st := g.state()
|
||||||
if st == nil {
|
if st == nil {
|
||||||
log.Warnf("ipc authz: DENY %s for %s, daemon state not attached", method, id)
|
log.Warnf("ipc authz: DENY %s for %s, daemon state not attached", method, id)
|
||||||
return ctx, status.Error(codes.Unavailable, "daemon not initialized")
|
return ctx, gstatus.Error(codes.Unavailable, "daemon not initialized")
|
||||||
}
|
}
|
||||||
policy := methodPolicyFor(method)
|
policy := methodPolicyFor(method)
|
||||||
|
|
||||||
@@ -159,14 +113,14 @@ func (g *AuthzGate) authorize(ctx context.Context, method string, msg any) (cont
|
|||||||
if policy.TargetsProfile {
|
if policy.TargetsProfile {
|
||||||
named, ok := targetProfile(msg)
|
named, ok := targetProfile(msg)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx, status.Errorf(codes.Internal, "%s is declared target-scoped but names no profile", method)
|
return ctx, gstatus.Errorf(codes.Internal, "%s is declared target-scoped but names no profile", method)
|
||||||
}
|
}
|
||||||
handle = named
|
handle = named
|
||||||
}
|
}
|
||||||
|
|
||||||
target, handleErr := st.ResolveTarget(id, handle)
|
target, handleErr := st.ResolveTarget(id, handle)
|
||||||
|
|
||||||
level := resolveLevel(id, target, st)
|
level := g.resolveLevel(id, target)
|
||||||
|
|
||||||
if handleErr != nil && handle != "" {
|
if handleErr != nil && handle != "" {
|
||||||
level = AuthzLevelIdentified
|
level = AuthzLevelIdentified
|
||||||
@@ -204,3 +158,27 @@ func (g *AuthzGate) authorize(ctx context.Context, method string, msg any) (cont
|
|||||||
}
|
}
|
||||||
return ContextWithTarget(ctx, target.Path), 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ipcauth
|
package ipcauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
gstatus "google.golang.org/grpc/status"
|
gstatus "google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,45 +47,47 @@ func (l AuthzLevel) String() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveLevel is the authority the caller holds over the profile the request
|
// RequireLevel builds a rule from a level, for composing inside another rule.
|
||||||
// resolved to. A profile the caller does not own confers nothing beyond being
|
func RequireLevel(want AuthzLevel) Rule {
|
||||||
// identified, which is also what an unresolved handle leaves them with.
|
return func(r Request) error {
|
||||||
func resolveLevel(id Identity, target Target, st DaemonState) AuthzLevel {
|
if r.Level >= want {
|
||||||
if !id.Known() {
|
return nil
|
||||||
return AuthzLevelNone
|
}
|
||||||
|
return denyLevel(r, want)
|
||||||
}
|
}
|
||||||
if IsPrivilegedCaller(id) {
|
|
||||||
return AuthzLevelPrivileged
|
|
||||||
}
|
|
||||||
if !target.Owned {
|
|
||||||
return AuthzLevelIdentified
|
|
||||||
}
|
|
||||||
if holder, running := st.SessionHolder(); !running || holder.Matches(id) {
|
|
||||||
return AuthzLevelSessionHolder
|
|
||||||
}
|
|
||||||
return AuthzLevelProfileOwner
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// presentableHandleError keeps a resolution failure only when the gate can put
|
func denyLevel(r Request, want AuthzLevel) error {
|
||||||
// it in front of the caller in place of its own refusal. Everything else is
|
return gstatus.Errorf(codes.PermissionDenied,
|
||||||
// dropped, and the caller gets the refusal their level earned.
|
"%s requires %s, caller %s is %s", r.Method, want, r.Identity, r.Level)
|
||||||
func presentableHandleError(handle string, err error) error {
|
}
|
||||||
if err == nil {
|
|
||||||
return nil
|
// denyPolicyLevel refuses a caller at the gate, where the policy is in hand.
|
||||||
}
|
//
|
||||||
|
// Requiring privilege is the one denial a caller can act on, so it carries the
|
||||||
|
// elevated command rather than a bare refusal. A privileged method that declares
|
||||||
|
// no action keeps the plain message. Rules deny through denyLevel instead: they
|
||||||
|
// cannot reach the policy table without an initialization cycle, and no rule
|
||||||
|
// requires privilege.
|
||||||
|
func denyPolicyLevel(r Request, p MethodPolicy) error {
|
||||||
|
switch p.Level {
|
||||||
|
case AuthzLevelPrivileged:
|
||||||
|
if p.Action != "" {
|
||||||
|
actor, command := RequiredActor(p.Command)
|
||||||
|
return PrivilegeError(PrivilegeSummary(p.Action, actor), command)
|
||||||
|
}
|
||||||
|
|
||||||
|
case AuthzLevelSessionHolder:
|
||||||
|
// resolveLevel stops at profile owner only when a session is running and
|
||||||
|
// somebody else holds it.
|
||||||
|
if r.Level == AuthzLevelProfileOwner {
|
||||||
|
return SessionHeldError(p.Action)
|
||||||
|
}
|
||||||
|
return NotOwnerError(p.Action)
|
||||||
|
|
||||||
// An empty handle is the active profile rather than something the caller
|
case AuthzLevelProfileOwner:
|
||||||
// typed, so a failure to resolve it is not theirs to correct.
|
return NotOwnerError(p.Action)
|
||||||
if handle == "" {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only a gRPC status reaches the caller as a sentence the CLI and the UI
|
return denyLevel(r, p.Level)
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package ipcauth
|
package ipcauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
)
|
||||||
|
|
||||||
const servicePath = "/daemon.DaemonService/"
|
const servicePath = "/daemon.DaemonService/"
|
||||||
|
|
||||||
// Request is what a rule decides on: the authorization plus the state and the
|
// Request is what a rule decides on: the authorization plus the state and the
|
||||||
@@ -50,6 +54,25 @@ type MethodPolicy struct {
|
|||||||
Command string
|
Command string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequireHolderForFullStatus escalates a StatusRequest that asks for peer detail
|
||||||
|
// or for probes to be run.
|
||||||
|
func RequireHolderForFullStatus(r Request) error {
|
||||||
|
statusReq, ok := r.Msg.(*proto.StatusRequest)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if r.Level < AuthzLevelSessionHolder {
|
||||||
|
if statusReq.GetFullPeerStatus {
|
||||||
|
statusReq.GetFullPeerStatus = false
|
||||||
|
}
|
||||||
|
if statusReq.ShouldRunProbes {
|
||||||
|
statusReq.ShouldRunProbes = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return RequireLevel(AuthzLevelSessionHolder)(r)
|
||||||
|
}
|
||||||
|
|
||||||
// methodPolicies is the complete authorization surface. Every RPC on
|
// methodPolicies is the complete authorization surface. Every RPC on
|
||||||
// DaemonService appears here exactly once.
|
// DaemonService appears here exactly once.
|
||||||
var methodPolicies = map[string]MethodPolicy{
|
var methodPolicies = map[string]MethodPolicy{
|
||||||
|
|||||||
Reference in New Issue
Block a user