mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-08 22:49:10 +02:00
Add authorization framework for gRPC methods
This commit is contained in:
@@ -46,7 +46,7 @@ type program struct {
|
|||||||
jsonServMu sync.Mutex
|
jsonServMu sync.Mutex
|
||||||
serverInstance *server.Server
|
serverInstance *server.Server
|
||||||
serverInstanceMu sync.Mutex
|
serverInstanceMu sync.Mutex
|
||||||
ruleGate *ipcauth.RuleGate
|
authzGate *ipcauth.AuthzGate
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -80,12 +80,12 @@ func (p *program) Start(svc service.Service) error {
|
|||||||
return fmt.Errorf("parse daemon address: %w", err)
|
return fmt.Errorf("parse daemon address: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.ruleGate = ipcauth.NewRuleGate()
|
p.authzGate = ipcauth.NewAuthzGate()
|
||||||
|
|
||||||
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
|
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
|
||||||
opts := append(daemonServerOptions(network),
|
opts := append(daemonServerOptions(network),
|
||||||
grpc.ChainUnaryInterceptor(p.ruleGate.UnaryPolicyInterceptor()),
|
grpc.ChainUnaryInterceptor(p.authzGate.UnaryPolicyInterceptor()),
|
||||||
grpc.ChainStreamInterceptor(p.ruleGate.StreamPolicyInterceptor()),
|
grpc.ChainStreamInterceptor(p.authzGate.StreamPolicyInterceptor()),
|
||||||
)
|
)
|
||||||
p.serv = grpc.NewServer(opts...)
|
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)
|
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, captureEnabled, networksDisabled)
|
||||||
p.ruleGate.SetState(serverInstance)
|
p.authzGate.SetState(serverInstance)
|
||||||
if err := serverInstance.Start(); err != nil {
|
if err := serverInstance.Start(); err != nil {
|
||||||
return fmt.Errorf("start daemon: %w", err)
|
return fmt.Errorf("start daemon: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package ipcauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"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)
|
||||||
|
|
||||||
|
OwnsProfile(id Identity, handle string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireHolderForFullStatus is a Rule that enforces the right AuthzLevel if
|
||||||
|
// "full status" or "should run probes" are requested in a StatusRequest.
|
||||||
|
func RequireHolderForFullStatus(r Request) error {
|
||||||
|
full, ok := r.Msg.(interface{ GetGetFullPeerStatus() bool })
|
||||||
|
if !ok || !full.GetGetFullPeerStatus() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
probes, ok := r.Msg.(interface{ ShouldRunProbes() bool })
|
||||||
|
if !ok || !probes.ShouldRunProbes() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireFlowInitiator binds a pending authentication flow to the identity that
|
||||||
|
// started it.
|
||||||
|
func RequireFlowInitiator(r Request) error {
|
||||||
|
// TODO: needs the flow registry keyed by initiator.
|
||||||
|
return status.Error(codes.Unimplemented, "pending flows are not yet caller-bound")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
authCtx, authErr := g.authorize(ss.Context(), info.FullMethod, nil)
|
||||||
|
if authErr != nil {
|
||||||
|
return authErr
|
||||||
|
}
|
||||||
|
return handler(srv, &authorizedStream{ServerStream: ss, ctx: authCtx})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
authCtx, authErr := g.authorize(ctx, info.FullMethod, req)
|
||||||
|
if authErr != nil {
|
||||||
|
return nil, authErr
|
||||||
|
}
|
||||||
|
return handler(authCtx, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 nil, status.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 nil, status.Error(codes.Unavailable, "daemon not initialized")
|
||||||
|
}
|
||||||
|
target, named := targetProfile(msg)
|
||||||
|
policy := methodPolicyFor(method)
|
||||||
|
if policy.TargetsProfile && !named {
|
||||||
|
return nil, status.Errorf(codes.Internal, "%s is declared target-scoped but names no profile", method)
|
||||||
|
}
|
||||||
|
auth := Authorization{
|
||||||
|
Identity: id,
|
||||||
|
Level: resolveLevel(id, target, st),
|
||||||
|
Target: target,
|
||||||
|
Method: method,
|
||||||
|
}
|
||||||
|
req := Request{Authorization: auth, 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)
|
||||||
|
return nil, denyLevel(req, policy.Level)
|
||||||
|
}
|
||||||
|
for _, rule := range policy.Rules {
|
||||||
|
if err := rule(req); err != nil {
|
||||||
|
log.Warnf("ipc authz: DENY %s for %s (%s): %v", method, id, req.Level, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if policy.Audit {
|
||||||
|
log.Infof("ipc authz: allow %s for %s (%s)", method, id, req.Level)
|
||||||
|
}
|
||||||
|
return withAuthorization(ctx, auth), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorizedStream carries the authorized context into a streaming handler,
|
||||||
|
// which would otherwise see the one the stream was created with.
|
||||||
|
type authorizedStream struct {
|
||||||
|
grpc.ServerStream
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
type authorizationKey struct{}
|
||||||
|
|
||||||
|
func withAuthorization(ctx context.Context, a Authorization) context.Context {
|
||||||
|
return context.WithValue(ctx, authorizationKey{}, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorized returns the decision the interceptor made for this RPC.
|
||||||
|
//
|
||||||
|
// Read it to decide what a caller sees or who an action is attributed to. Do not
|
||||||
|
// read it to decide whether a call is allowed, which is the method table's job.
|
||||||
|
func Authorized(ctx context.Context) Authorization {
|
||||||
|
a, _ := ctx.Value(authorizationKey{}).(Authorization)
|
||||||
|
return a
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package ipcauth
|
||||||
|
|
||||||
|
// AuthzLevel is the authority a caller holds over the daemon's current state.
|
||||||
|
// The values are ordered, and each level can do everything the levles below
|
||||||
|
// it can. A MethodPolicy is satisfied when the caller's level is at least
|
||||||
|
// the level the method requires.
|
||||||
|
type AuthzLevel uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AuthzLevelNone is a caller whose kernel identity could not be established.
|
||||||
|
AuthzLevelNone AuthzLevel = iota
|
||||||
|
|
||||||
|
// AuthzLevelIdentified is any caller the kernel could verify.
|
||||||
|
AuthzLevelIdentified
|
||||||
|
|
||||||
|
// AuthzLevelProfileOwner is a caller being the owner of the current targeted
|
||||||
|
// profile
|
||||||
|
AuthzLevelProfileOwner
|
||||||
|
|
||||||
|
// AuthzLevelSessionHolder is a caller that owns the current active profile
|
||||||
|
// and the session is currently connected (after running UP).
|
||||||
|
AuthzLevelSessionHolder
|
||||||
|
|
||||||
|
// AuthzLevelPrivileged is root, an elevated administrator or the daemon's
|
||||||
|
// own identity (if running as less privileges than root).
|
||||||
|
AuthzLevelPrivileged
|
||||||
|
)
|
||||||
|
|
||||||
|
// Strint() resolves a AuthzLevel to a human readable debug string.
|
||||||
|
func (l AuthzLevel) String() string {
|
||||||
|
switch l {
|
||||||
|
case AuthzLevelIdentified:
|
||||||
|
return "identified"
|
||||||
|
case AuthzLevelProfileOwner:
|
||||||
|
return "profile owner"
|
||||||
|
case AuthzLevelSessionHolder:
|
||||||
|
return "session holder"
|
||||||
|
case AuthzLevelPrivileged:
|
||||||
|
return "privileged"
|
||||||
|
default:
|
||||||
|
return "unidentified"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveLevel(id Identity, target string, st DaemonState) AuthzLevel {
|
||||||
|
if !id.Known() {
|
||||||
|
return AuthzLevelNone
|
||||||
|
}
|
||||||
|
if IsPrivilegedCaller(id) {
|
||||||
|
return AuthzLevelPrivileged
|
||||||
|
}
|
||||||
|
if !st.OwnsProfile(id, target) {
|
||||||
|
return AuthzLevelIdentified
|
||||||
|
}
|
||||||
|
if holder, running := st.SessionHolder(); !running || holder.Matches(id) {
|
||||||
|
return AuthzLevelSessionHolder
|
||||||
|
}
|
||||||
|
return AuthzLevelProfileOwner
|
||||||
|
}
|
||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The zero Identity carries uid 0, so every predicate that reads UID has to
|
// The zero Identity carries uid 0, so every predicate that reads UID has to
|
||||||
@@ -55,65 +53,6 @@ func TestKnownForTestMarksIdentity(t *testing.T) {
|
|||||||
assert.Equal(t, uint32(1000), id.UID, "KnownForTest must not alter the identity")
|
assert.Equal(t, uint32(1000), id.UID, "KnownForTest must not alter the identity")
|
||||||
}
|
}
|
||||||
|
|
||||||
type stubState struct {
|
|
||||||
holder Principal
|
|
||||||
held bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s stubState) SessionHolder() (Principal, bool) { return s.holder, s.held }
|
|
||||||
|
|
||||||
func uidHolder(uid uint32) Principal {
|
|
||||||
p, ok := ParsePrincipal(UIDPrincipal(uid))
|
|
||||||
if !ok {
|
|
||||||
panic("bad uid principal")
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// The holder comes from a profile JSON, the caller from a peercred read. When
|
|
||||||
// both were an Identity, the known marker made every such comparison false and
|
|
||||||
// the session holder could never be recognised.
|
|
||||||
func TestRequireSessionHolderMatchesConfigOwner(t *testing.T) {
|
|
||||||
asDaemon(t, KnownForTest(Identity{UID: 0}))
|
|
||||||
|
|
||||||
caller := KnownForTest(Identity{UID: 1000, GID: 1000})
|
|
||||||
assert.NoError(t, RequireSessionHolder(caller, stubState{holder: uidHolder(1000), held: true}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRequireSessionHolderRejectsAnotherUser(t *testing.T) {
|
|
||||||
asDaemon(t, KnownForTest(Identity{UID: 0}))
|
|
||||||
|
|
||||||
caller := KnownForTest(Identity{UID: 1000, GID: 1000})
|
|
||||||
err := RequireSessionHolder(caller, stubState{holder: uidHolder(1001), held: true})
|
|
||||||
assert.Equal(t, codes.PermissionDenied, status.Code(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// With nobody connected there is no session to protect.
|
|
||||||
func TestRequireSessionHolderAllowsWhenUnheld(t *testing.T) {
|
|
||||||
asDaemon(t, KnownForTest(Identity{UID: 0}))
|
|
||||||
|
|
||||||
caller := KnownForTest(Identity{UID: 1000, GID: 1000})
|
|
||||||
assert.NoError(t, RequireSessionHolder(caller, stubState{held: false}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// An owner that could not be parsed leaves the zero Principal, which matches
|
|
||||||
// nobody, so the session locks rather than opening.
|
|
||||||
func TestRequireSessionHolderLocksOnUnparseableOwner(t *testing.T) {
|
|
||||||
asDaemon(t, KnownForTest(Identity{UID: 0}))
|
|
||||||
|
|
||||||
caller := KnownForTest(Identity{UID: 1000, GID: 1000})
|
|
||||||
err := RequireSessionHolder(caller, stubState{holder: Principal{}, held: true})
|
|
||||||
assert.Equal(t, codes.PermissionDenied, status.Code(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root takes over whoever holds the session.
|
|
||||||
func TestRequireSessionHolderAllowsPrivilegedCaller(t *testing.T) {
|
|
||||||
asDaemon(t, KnownForTest(Identity{UID: 1000}))
|
|
||||||
|
|
||||||
root := KnownForTest(Identity{UID: 0})
|
|
||||||
assert.NoError(t, RequireSessionHolder(root, stubState{holder: uidHolder(1001), held: true}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// A uid:0 owner is a config value, so it grants nothing on its own.
|
// A uid:0 owner is a config value, so it grants nothing on its own.
|
||||||
func TestConfigOwnerCannotGrantPrivilege(t *testing.T) {
|
func TestConfigOwnerCannotGrantPrivilege(t *testing.T) {
|
||||||
root, ok := ParsePrincipal("uid:0")
|
root, ok := ParsePrincipal("uid:0")
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package ipcauth
|
||||||
|
|
||||||
|
const servicePath = "/daemon.DaemonService/"
|
||||||
|
|
||||||
|
// Authorization is the decision the interceptor reached for one RPC. It is what
|
||||||
|
// handlers read.
|
||||||
|
type Authorization struct {
|
||||||
|
Identity Identity
|
||||||
|
Level AuthzLevel
|
||||||
|
Target string
|
||||||
|
Method string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request is what a rule decides on: the authorization plus the state and the
|
||||||
|
// message, which only the gate needs.
|
||||||
|
type Request struct {
|
||||||
|
Authorization
|
||||||
|
State DaemonState
|
||||||
|
Method string
|
||||||
|
Msg any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule is an additional constraint beyond the method's level. Every rule on a
|
||||||
|
// method must pass.
|
||||||
|
type Rule func(Request) error
|
||||||
|
|
||||||
|
// The generated getters the profile RPCs expose.
|
||||||
|
type handleTargeted interface{ GetHandle() string }
|
||||||
|
type profileTargeted interface{ GetProfileName() string }
|
||||||
|
|
||||||
|
// targetProfile returns the profile a request names, and whether it carries a
|
||||||
|
// target field at all. Requests with no target act on the active profile.
|
||||||
|
func targetProfile(msg any) (string, bool) {
|
||||||
|
switch m := msg.(type) {
|
||||||
|
case handleTargeted:
|
||||||
|
return m.GetHandle(), true
|
||||||
|
case profileTargeted:
|
||||||
|
return m.GetProfileName(), true
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MethodPolicy is what a method requires to be authorized and then handled.
|
||||||
|
type MethodPolicy struct {
|
||||||
|
Level AuthzLevel
|
||||||
|
Rules []Rule
|
||||||
|
Audit bool
|
||||||
|
TargetsProfile bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// methodPolicies is the complete authorization surface. Every RPC on
|
||||||
|
// DaemonService appears here exactly once.
|
||||||
|
var methodPolicies = map[string]MethodPolicy{
|
||||||
|
// Any identified caller.
|
||||||
|
servicePath + "Status": {Level: AuthzLevelIdentified, Rules: []Rule{RequireHolderForFullStatus}},
|
||||||
|
servicePath + "AddProfile": {Level: AuthzLevelIdentified, Audit: true},
|
||||||
|
servicePath + "GetActiveProfile": {Level: AuthzLevelIdentified},
|
||||||
|
servicePath + "GetFeatures": {Level: AuthzLevelIdentified},
|
||||||
|
servicePath + "WailsUIReady": {Level: AuthzLevelIdentified},
|
||||||
|
|
||||||
|
// Pending flows: bound to the principal that started them, at any level.
|
||||||
|
servicePath + "WaitSSOLogin": {Level: AuthzLevelIdentified, Rules: []Rule{RequireFlowInitiator}, Audit: true},
|
||||||
|
servicePath + "WaitJWTToken": {Level: AuthzLevelIdentified, Rules: []Rule{RequireFlowInitiator}, Audit: true},
|
||||||
|
servicePath + "WaitExtendAuthSession": {Level: AuthzLevelIdentified, Rules: []Rule{RequireFlowInitiator}},
|
||||||
|
|
||||||
|
// Owner of the profile the request names.
|
||||||
|
servicePath + "GetConfig": {Level: AuthzLevelProfileOwner, TargetsProfile: true, Audit: true},
|
||||||
|
servicePath + "SetConfig": {Level: AuthzLevelProfileOwner, TargetsProfile: true, Audit: true},
|
||||||
|
servicePath + "Login": {Level: AuthzLevelSessionHolder, TargetsProfile: true, Audit: true},
|
||||||
|
servicePath + "Logout": {Level: AuthzLevelProfileOwner, TargetsProfile: true, Audit: true},
|
||||||
|
servicePath + "RenameProfile": {Level: AuthzLevelProfileOwner, TargetsProfile: true},
|
||||||
|
servicePath + "RemoveProfile": {Level: AuthzLevelProfileOwner, TargetsProfile: true, Audit: true},
|
||||||
|
servicePath + "SwitchProfile": {Level: AuthzLevelSessionHolder, TargetsProfile: true, Audit: true},
|
||||||
|
|
||||||
|
// Owner of some profile
|
||||||
|
servicePath + "ListProfiles": {Level: AuthzLevelProfileOwner},
|
||||||
|
servicePath + "GetLogLevel": {Level: AuthzLevelProfileOwner},
|
||||||
|
servicePath + "ListStates": {Level: AuthzLevelProfileOwner},
|
||||||
|
servicePath + "GetInstallerResult": {Level: AuthzLevelProfileOwner},
|
||||||
|
|
||||||
|
// Session holder: the live engine and everything daemon-wide.
|
||||||
|
servicePath + "Up": {Level: AuthzLevelSessionHolder, TargetsProfile: true, Audit: true},
|
||||||
|
servicePath + "Down": {Level: AuthzLevelSessionHolder, Audit: true},
|
||||||
|
servicePath + "SubscribeStatus": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "SubscribeEvents": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "GetEvents": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "ListNetworks": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "SelectNetworks": {Level: AuthzLevelSessionHolder, Audit: true},
|
||||||
|
servicePath + "DeselectNetworks": {Level: AuthzLevelSessionHolder, Audit: true},
|
||||||
|
servicePath + "ForwardingRules": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "ExposeService": {Level: AuthzLevelSessionHolder, Audit: true},
|
||||||
|
servicePath + "GetPeerSSHHostKey": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "RequestJWTAuth": {Level: AuthzLevelSessionHolder, Audit: true},
|
||||||
|
servicePath + "RequestExtendAuthSession": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "DismissSessionWarning": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "DebugBundle": {Level: AuthzLevelSessionHolder, Audit: true},
|
||||||
|
servicePath + "SetLogLevel": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "SetSyncResponsePersistence": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "StartCapture": {Level: AuthzLevelSessionHolder, Audit: true},
|
||||||
|
servicePath + "StartBundleCapture": {Level: AuthzLevelSessionHolder, Audit: true},
|
||||||
|
servicePath + "StopBundleCapture": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "StartCPUProfile": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "StopCPUProfile": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "CleanState": {Level: AuthzLevelSessionHolder, Audit: true},
|
||||||
|
servicePath + "DeleteState": {Level: AuthzLevelSessionHolder, Audit: true},
|
||||||
|
servicePath + "TracePacket": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "RegisterUILog": {Level: AuthzLevelSessionHolder},
|
||||||
|
servicePath + "TriggerUpdate": {Level: AuthzLevelSessionHolder, Audit: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
func methodPolicyFor(method string) MethodPolicy {
|
||||||
|
if p, ok := methodPolicies[method]; ok {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
// TODO: reconsider falling back to Privileged rather than direct DENY.
|
||||||
|
return MethodPolicy{Level: AuthzLevelPrivileged, Audit: true}
|
||||||
|
}
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package ipcauth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DaemonState interface {
|
|
||||||
// SessionHolder returns the principal entitled to the live connection and
|
|
||||||
// whether one is held. A held session whose owner cannot be parsed returns
|
|
||||||
// the zero Principal with true, which matches nobody, so a corrupt owner
|
|
||||||
// field locks the session instead of opening it.
|
|
||||||
SessionHolder() (Principal, bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Rule func(id Identity, st DaemonState) error
|
|
||||||
|
|
||||||
type RuleGate struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
rules []Rule
|
|
||||||
st DaemonState
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRuleGate() *RuleGate {
|
|
||||||
return &RuleGate{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *RuleGate) SetState(st DaemonState) {
|
|
||||||
g.mu.Lock()
|
|
||||||
defer g.mu.Unlock()
|
|
||||||
g.st = st
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *RuleGate) SetRule(r Rule) {
|
|
||||||
g.mu.Lock()
|
|
||||||
defer g.mu.Unlock()
|
|
||||||
g.rules = append(g.rules, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *RuleGate) state() DaemonState {
|
|
||||||
g.mu.Lock()
|
|
||||||
defer g.mu.Unlock()
|
|
||||||
return g.st
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequireSessionHolder allows the caller to act on the live connection. With no
|
|
||||||
// session held there is nothing to protect, and root can always take over.
|
|
||||||
func RequireSessionHolder(id Identity, st DaemonState) error {
|
|
||||||
holder, running := st.SessionHolder()
|
|
||||||
if !running || IsPrivilegedCaller(id) || holder.Matches(id) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
log.Debugf("caller %v is not the session holder %v", id, holder)
|
|
||||||
return status.Errorf(codes.PermissionDenied, "session is held by another user (%v)", holder)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *RuleGate) StreamPolicyInterceptor() grpc.StreamServerInterceptor {
|
|
||||||
return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
|
||||||
if authErr := g.authorize(ss.Context()); authErr != nil {
|
|
||||||
return authErr
|
|
||||||
}
|
|
||||||
return handler(ss.Context(), ss)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *RuleGate) UnaryPolicyInterceptor() grpc.UnaryServerInterceptor {
|
|
||||||
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
|
|
||||||
if authErr := g.authorize(ctx); authErr != nil {
|
|
||||||
return nil, authErr
|
|
||||||
}
|
|
||||||
return handler(ctx, req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *RuleGate) authorize(ctx context.Context) error {
|
|
||||||
id, ok := CallerIdentity(ctx)
|
|
||||||
if !ok {
|
|
||||||
return status.Error(codes.PermissionDenied, "caller cannot be verified")
|
|
||||||
}
|
|
||||||
state := g.state()
|
|
||||||
for _, rule := range g.rules {
|
|
||||||
ruleErr := rule(id, state)
|
|
||||||
if ruleErr != nil {
|
|
||||||
return ruleErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -2723,6 +2723,11 @@ func (s *Server) SessionHolder() (ipcauth.Principal, bool) {
|
|||||||
return principal, true
|
return principal, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) OwnsProfile(id ipcauth.Identity, handle string) bool {
|
||||||
|
// TODO
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func persistLoginOverrides(activeProf *profilemanager.ActiveProfileState, managementURL string, preSharedKey *string) error {
|
func persistLoginOverrides(activeProf *profilemanager.ActiveProfileState, managementURL string, preSharedKey *string) error {
|
||||||
if preSharedKey != nil && *preSharedKey == "" {
|
if preSharedKey != nil && *preSharedKey == "" {
|
||||||
preSharedKey = nil
|
preSharedKey = nil
|
||||||
|
|||||||
Reference in New Issue
Block a user