mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-21 06:09:07 +02:00
WIP: acl interceptor and named pipe ui
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
gstatus "google.golang.org/grpc/status"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
)
|
||||
|
||||
// The daemon Server implements ipcauth.ProfilePolicy so the gRPC interceptor can
|
||||
// authorize each RPC against the active profile's ownership.
|
||||
var _ ipcauth.ProfilePolicy = (*Server)(nil)
|
||||
|
||||
// ActiveProfileOwnership returns the active profile's ownership policy. Reads
|
||||
// the in-memory active config (kept current by the handlers), falling back to
|
||||
// the on-disk active profile when the daemon hasn't loaded one yet.
|
||||
func (s *Server) ActiveProfileOwnership() ipcauth.Ownership {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
cfg := s.config
|
||||
if cfg == nil {
|
||||
loaded, err := s.loadActiveProfileConfigLocked()
|
||||
if err != nil {
|
||||
log.Warnf("ownership: cannot load active profile config, treating as unowned: %v", err)
|
||||
return ipcauth.Ownership{}
|
||||
}
|
||||
cfg = loaded
|
||||
}
|
||||
return ipcauth.Ownership{Owners: cfg.Owners, Shared: cfg.Shared}
|
||||
}
|
||||
|
||||
// ClaimActiveProfileOwnerIfUnowned atomically claims the active profile for id
|
||||
// when it has no owners and is not shared (trust-on-first-use). Returns whether
|
||||
// id is now an owner. Concurrent first-callers are serialized by s.mutex, so
|
||||
// exactly one wins the claim; the others get false and are authorized normally.
|
||||
func (s *Server) ClaimActiveProfileOwnerIfUnowned(id ipcauth.Identity) (bool, error) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
cfg := s.config
|
||||
if cfg == nil {
|
||||
loaded, err := s.loadActiveProfileConfigLocked()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("load active profile config: %w", err)
|
||||
}
|
||||
cfg = loaded
|
||||
}
|
||||
|
||||
if len(cfg.Owners) > 0 || cfg.Shared {
|
||||
return false, nil // already owned or shared — someone won the race
|
||||
}
|
||||
|
||||
cfg.Owners = []string{ipcauth.OwnerPrincipalForIdentity(id)}
|
||||
if err := s.persistActiveProfileConfigLocked(cfg); err != nil {
|
||||
cfg.Owners = nil // revert in-memory on persistence failure
|
||||
return false, fmt.Errorf("persist claimed ownership: %w", err)
|
||||
}
|
||||
s.config = cfg
|
||||
log.Infof("profile ownership claimed by %s (trust-on-first-use)", id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// activeProfileConfigPathLocked resolves the active profile's config file path.
|
||||
func (s *Server) activeProfileConfigPathLocked() (string, error) {
|
||||
activeProf, err := s.profileManager.GetActiveProfileState()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get active profile: %w", err)
|
||||
}
|
||||
path, err := activeProf.FilePath()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve active profile path: %w", err)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// loadActiveProfileConfigLocked reads the active profile's config from disk.
|
||||
func (s *Server) loadActiveProfileConfigLocked() (*profilemanager.Config, error) {
|
||||
path, err := s.activeProfileConfigPathLocked()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profilemanager.GetConfig(path)
|
||||
}
|
||||
|
||||
// persistActiveProfileConfigLocked writes cfg to the active profile's config file.
|
||||
func (s *Server) persistActiveProfileConfigLocked(cfg *profilemanager.Config) error {
|
||||
path, err := s.activeProfileConfigPathLocked()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.WriteJson(context.Background(), path, cfg)
|
||||
}
|
||||
|
||||
// activeConfigLocked returns the in-memory active config, loading it from disk
|
||||
// if the daemon hasn't cached one. Caller must hold s.mutex.
|
||||
func (s *Server) activeConfigLocked() (*profilemanager.Config, error) {
|
||||
if s.config != nil {
|
||||
return s.config, nil
|
||||
}
|
||||
return s.loadActiveProfileConfigLocked()
|
||||
}
|
||||
|
||||
// claimForCallerLocked adds the caller's principal to cfg (if absent) and
|
||||
// persists. No-op for privileged callers (they need no ownership entry). Caller
|
||||
// must hold s.mutex.
|
||||
func (s *Server) claimForCallerLocked(id ipcauth.Identity, cfg *profilemanager.Config) error {
|
||||
if id.IsPrivileged() {
|
||||
return nil
|
||||
}
|
||||
principal := ipcauth.OwnerPrincipalForIdentity(id)
|
||||
if slices.Contains(cfg.Owners, principal) {
|
||||
return nil
|
||||
}
|
||||
cfg.Owners = append(cfg.Owners, principal)
|
||||
if err := s.persistActiveProfileConfigLocked(cfg); err != nil {
|
||||
cfg.Owners = cfg.Owners[:len(cfg.Owners)-1] // revert on failure
|
||||
return err
|
||||
}
|
||||
s.config = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddOwner adds a principal to the active profile's owner list. The interceptor
|
||||
// has already confirmed the caller is an owner or privileged; the handler just
|
||||
// validates and persists.
|
||||
func (s *Server) AddOwner(_ context.Context, msg *proto.AddOwnerRequest) (*proto.AddOwnerResponse, error) {
|
||||
principal := msg.GetPrincipal()
|
||||
if _, ok := ipcauth.ParsePrincipal(principal); !ok {
|
||||
return nil, gstatus.Errorf(codes.InvalidArgument, "invalid owner principal %q (expected uid:/gid:/group:/sid:)", principal)
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
cfg, err := s.activeConfigLocked()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load active profile config: %w", err)
|
||||
}
|
||||
if slices.Contains(cfg.Owners, principal) {
|
||||
return &proto.AddOwnerResponse{}, nil
|
||||
}
|
||||
cfg.Owners = append(cfg.Owners, principal)
|
||||
if err := s.persistActiveProfileConfigLocked(cfg); err != nil {
|
||||
cfg.Owners = cfg.Owners[:len(cfg.Owners)-1]
|
||||
return nil, fmt.Errorf("persist owner: %w", err)
|
||||
}
|
||||
s.config = cfg
|
||||
log.Infof("added owner %q to the active profile", principal)
|
||||
return &proto.AddOwnerResponse{}, nil
|
||||
}
|
||||
|
||||
// ResetOwner clears the active profile's owner list (and shared flag), returning
|
||||
// it to the unowned state so the next caller re-claims via trust-on-first-use.
|
||||
// Privileged-only, so co-owners cannot evict each other.
|
||||
func (s *Server) ResetOwner(ctx context.Context, _ *proto.ResetOwnerRequest) (*proto.ResetOwnerResponse, error) {
|
||||
id, ok := ipcauth.IdentityFromContext(ctx)
|
||||
if !ok || !id.IsPrivileged() {
|
||||
return nil, gstatus.Error(codes.PermissionDenied, "reset-owner requires root/administrator")
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
cfg, err := s.activeConfigLocked()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load active profile config: %w", err)
|
||||
}
|
||||
cfg.Owners = nil
|
||||
cfg.Shared = false
|
||||
if err := s.persistActiveProfileConfigLocked(cfg); err != nil {
|
||||
return nil, fmt.Errorf("persist owner reset: %w", err)
|
||||
}
|
||||
s.config = cfg
|
||||
log.Infof("active profile owner list reset; next caller will re-claim (trust-on-first-use)")
|
||||
return &proto.ResetOwnerResponse{}, nil
|
||||
}
|
||||
|
||||
// ShareProfile marks the active profile shared or unshared. The interceptor has
|
||||
// already confirmed the caller is an owner or privileged.
|
||||
func (s *Server) ShareProfile(_ context.Context, msg *proto.ShareProfileRequest) (*proto.ShareProfileResponse, error) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
cfg, err := s.activeConfigLocked()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load active profile config: %w", err)
|
||||
}
|
||||
cfg.Shared = msg.GetShared()
|
||||
if err := s.persistActiveProfileConfigLocked(cfg); err != nil {
|
||||
return nil, fmt.Errorf("persist shared flag: %w", err)
|
||||
}
|
||||
s.config = cfg
|
||||
log.Infof("active profile shared flag set to %t", msg.GetShared())
|
||||
return &proto.ShareProfileResponse{}, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
gstatus "google.golang.org/grpc/status"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
)
|
||||
|
||||
// bindCallerUsername enforces that a non-privileged caller may only operate on
|
||||
// its OWN user's profiles. Profiles live in per-username directories, but the
|
||||
// username is a client-supplied gRPC field the server historically never
|
||||
// checked; binding it to the caller's kernel identity closes the "pass another
|
||||
// user's username" hole. Privileged callers (root / elevated-admin) may manage
|
||||
// any user's profiles.
|
||||
func (s *Server) bindCallerUsername(ctx context.Context, requested string) error {
|
||||
if requested == "" {
|
||||
return nil // handlers validate emptiness themselves; nothing to bind
|
||||
}
|
||||
|
||||
id, ok := ipcauth.IdentityFromContext(ctx)
|
||||
if !ok {
|
||||
return gstatus.Error(codes.PermissionDenied, "caller identity could not be verified")
|
||||
}
|
||||
if id.IsPrivileged() {
|
||||
return nil
|
||||
}
|
||||
|
||||
caller, err := usernameForIdentity(id)
|
||||
if err != nil {
|
||||
log.Warnf("profile authz: resolve caller username for %s: %v", id, err)
|
||||
return gstatus.Error(codes.PermissionDenied, "could not resolve caller identity to a username")
|
||||
}
|
||||
if !usernamesEqual(requested, caller) {
|
||||
return gstatus.Errorf(codes.PermissionDenied,
|
||||
"not authorized to operate on another user's profiles (caller %q requested %q)", caller, requested)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// usernamesEqual compares usernames case-insensitively on Windows (domain
|
||||
// accounts) and exactly on Unix.
|
||||
func usernamesEqual(a, b string) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
return strings.EqualFold(a, b)
|
||||
}
|
||||
return a == b
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//go:build !windows
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
"github.com/netbirdio/netbird/client/internal/shell"
|
||||
)
|
||||
|
||||
// usernameForIdentity resolves a Unix caller's UID to its username via NSS
|
||||
// (getent), so LDAP/AD users resolve correctly under CGO_ENABLED=0.
|
||||
func usernameForIdentity(id ipcauth.Identity) (string, error) {
|
||||
u, err := shell.GetUserFromGetent(strconv.FormatUint(uint64(id.UID), 10))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return u.Username, nil
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//go:build windows
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"os/user"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
)
|
||||
|
||||
// usernameForIdentity resolves a Windows caller's SID to its account name.
|
||||
func usernameForIdentity(id ipcauth.Identity) (string, error) {
|
||||
u, err := user.LookupId(id.SID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return u.Username, nil
|
||||
}
|
||||
+58
-1
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/auth"
|
||||
"github.com/netbirdio/netbird/client/internal/expose"
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
sleephandler "github.com/netbirdio/netbird/client/internal/sleep/handler"
|
||||
"github.com/netbirdio/netbird/client/mdm"
|
||||
@@ -402,6 +403,12 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques
|
||||
}
|
||||
}
|
||||
|
||||
// M-SSHGATE: turning on SSH root login / disabling SSH auth requires a
|
||||
// privileged caller (root or elevated admin).
|
||||
if err := requirePrivilegedForDangerousSSH(callerCtx, msg.EnableSSHRoot, msg.DisableSSHAuth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// MDM gate: refuse the whole request if any of its fields is enforced
|
||||
// by the active MDM policy. The error carries an MDMManagedFields-
|
||||
// Violation detail listing the offending key names. Non-conflicting
|
||||
@@ -537,6 +544,12 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
|
||||
}
|
||||
}
|
||||
|
||||
// M-SSHGATE: turning on SSH root login / disabling SSH auth requires a
|
||||
// privileged caller (root or elevated admin).
|
||||
if err := requirePrivilegedForDangerousSSH(callerCtx, msg.EnableSSHRoot, msg.DisableSSHAuth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
if s.actCancel != nil {
|
||||
s.actCancel()
|
||||
@@ -943,6 +956,17 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
|
||||
}
|
||||
s.config = config
|
||||
|
||||
// --owner: explicitly claim ownership of the active profile for the caller.
|
||||
if msg != nil && msg.GetClaimOwner() {
|
||||
if id, ok := ipcauth.IdentityFromContext(callerCtx); ok {
|
||||
if err := s.claimForCallerLocked(id, s.config); err != nil {
|
||||
s.mutex.Unlock()
|
||||
log.Errorf("failed to claim profile ownership: %v", err)
|
||||
return nil, fmt.Errorf("claim owner: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String())
|
||||
s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive)
|
||||
|
||||
@@ -1048,6 +1072,16 @@ func (s *Server) SwitchProfile(callerCtx context.Context, msg *proto.SwitchProfi
|
||||
}
|
||||
|
||||
if msg != nil && msg.ProfileName != nil {
|
||||
targetUsername := ""
|
||||
if msg.Username != nil {
|
||||
targetUsername = *msg.Username
|
||||
}
|
||||
// The interceptor already gated this against the CURRENT active profile;
|
||||
// also bind the TARGET profile's username so a caller can't switch into
|
||||
// another user's profile.
|
||||
if err := s.bindCallerUsername(callerCtx, targetUsername); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := s.switchProfileIfNeeded(*msg.ProfileName, msg.Username, activeProf); err != nil {
|
||||
log.Errorf("failed to switch profile: %v", err)
|
||||
return nil, err
|
||||
@@ -2005,7 +2039,18 @@ func (s *Server) AddProfile(ctx context.Context, msg *proto.AddProfileRequest) (
|
||||
return nil, gstatus.Errorf(codes.InvalidArgument, "profile name and username must be provided")
|
||||
}
|
||||
|
||||
created, err := s.profileManager.AddProfile(msg.ProfileName, msg.Username)
|
||||
if err := s.bindCallerUsername(ctx, msg.Username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Auto-isolate the new profile to its creator. When root/admin creates it we
|
||||
// leave it unowned so the intended user claims it via trust-on-first-use.
|
||||
var initialOwners []string
|
||||
if id, ok := ipcauth.IdentityFromContext(ctx); ok && !id.IsPrivileged() {
|
||||
initialOwners = []string{ipcauth.OwnerPrincipalForIdentity(id)}
|
||||
}
|
||||
|
||||
created, err := s.profileManager.AddProfile(msg.ProfileName, msg.Username, initialOwners)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create profile: %v", err)
|
||||
return nil, fmt.Errorf("failed to create profile: %w", err)
|
||||
@@ -2028,6 +2073,10 @@ func (s *Server) RenameProfile(ctx context.Context, msg *proto.RenameProfileRequ
|
||||
return nil, gstatus.Errorf(codes.InvalidArgument, "profile name, username and new profile name must be provided")
|
||||
}
|
||||
|
||||
if err := s.bindCallerUsername(ctx, msg.Username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolved, err := s.resolveProfileHandle(msg.Handle, msg.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2057,6 +2106,10 @@ func (s *Server) RemoveProfile(ctx context.Context, msg *proto.RemoveProfileRequ
|
||||
return nil, gstatus.Errorf(codes.InvalidArgument, "profile name must be provided")
|
||||
}
|
||||
|
||||
if err := s.bindCallerUsername(ctx, msg.Username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolved, err := s.resolveProfileHandle(msg.ProfileName, msg.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2125,6 +2178,10 @@ func (s *Server) ListProfiles(ctx context.Context, msg *proto.ListProfilesReques
|
||||
return nil, gstatus.Errorf(codes.InvalidArgument, "username must be provided")
|
||||
}
|
||||
|
||||
if err := s.bindCallerUsername(ctx, msg.Username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
profiles, err := s.profileManager.ListProfiles(msg.Username)
|
||||
if err != nil {
|
||||
log.Errorf("failed to list profiles: %v", err)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
gstatus "google.golang.org/grpc/status"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
)
|
||||
|
||||
// requirePrivilegedForDangerousSSH enforces M-SSHGATE: enabling SSH root login
|
||||
// or disabling SSH authentication turns the root/LocalSystem daemon's SSH server
|
||||
// into an unauthenticated root shell (local-to-remote-root escalation), so only
|
||||
// a privileged caller (Unix root, or Windows elevated-admin/LocalSystem) may set
|
||||
// these flags to true over the local IPC.
|
||||
//
|
||||
// It gates the request fields, not the resulting config: a value already
|
||||
// persisted (e.g. set previously by root, or by MDM/managed config) is untouched;
|
||||
// only a new attempt to turn them on via SetConfig/Login is checked.
|
||||
//
|
||||
// When the caller identity cannot be verified (e.g. the daemon is on a TCP
|
||||
// control channel with no peer credentials) it fails closed — refusing rather
|
||||
// than letting an unauthenticated local caller flip these flags.
|
||||
func requirePrivilegedForDangerousSSH(ctx context.Context, enableSSHRoot, disableSSHAuth *bool) error {
|
||||
dangerous := (enableSSHRoot != nil && *enableSSHRoot) || (disableSSHAuth != nil && *disableSSHAuth)
|
||||
if !dangerous {
|
||||
return nil
|
||||
}
|
||||
|
||||
id, ok := ipcauth.IdentityFromContext(ctx)
|
||||
if !ok {
|
||||
log.Warnf("denying SSH root/no-auth config change: caller identity unavailable on this control channel")
|
||||
return gstatus.Error(codes.PermissionDenied,
|
||||
"enabling SSH root login or disabling SSH authentication requires root/administrator, but the caller identity could not be verified on this daemon control channel")
|
||||
}
|
||||
if !id.IsPrivileged() {
|
||||
log.Warnf("denying SSH root/no-auth config change from non-privileged caller %s", id)
|
||||
return gstatus.Errorf(codes.PermissionDenied,
|
||||
"enabling SSH root login or disabling SSH authentication requires root/administrator (caller %s is not privileged); rerun as root/administrator", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/peer"
|
||||
gstatus "google.golang.org/grpc/status"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
)
|
||||
|
||||
func ctxWithIdentity(id ipcauth.Identity) context.Context {
|
||||
return peer.NewContext(context.Background(), &peer.Peer{AuthInfo: ipcauth.AuthInfo{Identity: id}})
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
|
||||
func TestRequirePrivilegedForDangerousSSH(t *testing.T) {
|
||||
root := ipcauth.Identity{UID: 0}
|
||||
user := ipcauth.Identity{UID: 1000}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
enableSSHRoot *bool
|
||||
disableSSHAuth *bool
|
||||
wantDenied bool
|
||||
}{
|
||||
{"no flags, no identity", context.Background(), nil, nil, false},
|
||||
{"flags false, non-priv", ctxWithIdentity(user), boolPtr(false), boolPtr(false), false},
|
||||
{"enableSSHRoot by root", ctxWithIdentity(root), boolPtr(true), nil, false},
|
||||
{"enableSSHRoot by non-priv", ctxWithIdentity(user), boolPtr(true), nil, true},
|
||||
{"disableSSHAuth by non-priv", ctxWithIdentity(user), nil, boolPtr(true), true},
|
||||
{"enableSSHRoot no identity (fail closed)", context.Background(), boolPtr(true), nil, true},
|
||||
{"both by root", ctxWithIdentity(root), boolPtr(true), boolPtr(true), false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := requirePrivilegedForDangerousSSH(tt.ctx, tt.enableSSHRoot, tt.disableSSHAuth)
|
||||
if tt.wantDenied {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, codes.PermissionDenied, gstatus.Code(err))
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user