mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
Add WIP profile ownership
This commit is contained in:
@@ -13,6 +13,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/peer"
|
||||
@@ -138,3 +140,49 @@ func IdentityFromContext(ctx context.Context) (Identity, bool) {
|
||||
}
|
||||
return info.Identity, true
|
||||
}
|
||||
|
||||
// PrincipalKind is the type of an owner principal.
|
||||
type PrincipalKind string
|
||||
|
||||
const (
|
||||
KindUID PrincipalKind = "uid" // Unix user ID
|
||||
KindSID PrincipalKind = "sid" // Windows user or group SID
|
||||
)
|
||||
|
||||
// Principal is a parsed owner entry from a profile's Owners list.
|
||||
type Principal struct {
|
||||
Kind PrincipalKind
|
||||
Value string
|
||||
}
|
||||
|
||||
// ParsePrincipal parses a "kind:value" owner string. Returns false for empty
|
||||
// values or unknown kinds so malformed entries are ignored rather than trusted.
|
||||
func ParsePrincipal(s string) (Principal, bool) {
|
||||
kind, value, ok := strings.Cut(s, ":")
|
||||
if !ok || value == "" {
|
||||
return Principal{}, false
|
||||
}
|
||||
switch PrincipalKind(kind) {
|
||||
case KindUID, KindSID:
|
||||
return Principal{Kind: PrincipalKind(kind), Value: value}, true
|
||||
default:
|
||||
return Principal{}, false
|
||||
}
|
||||
}
|
||||
|
||||
// UIDPrincipal builds the owner string for a Unix user ID.
|
||||
func UIDPrincipal(uid uint32) string {
|
||||
return string(KindUID) + ":" + strconv.FormatUint(uint64(uid), 10)
|
||||
}
|
||||
|
||||
// SIDPrincipal builds the owner string for a Windows SID.
|
||||
func SIDPrincipal(sid string) string { return string(KindSID) + ":" + sid }
|
||||
|
||||
// OwnerPrincipalForIdentity returns the self-ownership principal for an identity:
|
||||
// the user's UID on Unix, or the user's SID on Windows.
|
||||
func OwnerPrincipalForIdentity(id Identity) string {
|
||||
if id.IsWindows() {
|
||||
return SIDPrincipal(id.SID)
|
||||
}
|
||||
return UIDPrincipal(id.UID)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/iface"
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
|
||||
"github.com/netbirdio/netbird/client/mdm"
|
||||
"github.com/netbirdio/netbird/client/ssh"
|
||||
@@ -107,6 +108,7 @@ type ConfigInput struct {
|
||||
|
||||
LocalMetricsEnabled *bool
|
||||
LocalMetricsAddress *string
|
||||
Owner *ipcauth.Identity
|
||||
}
|
||||
|
||||
// Config Configuration type
|
||||
@@ -208,6 +210,8 @@ type Config struct {
|
||||
// Callers query enforcement state via Policy() and the mdm.Policy API
|
||||
// (HasKey, ManagedKeys, IsEmpty).
|
||||
policy *mdm.Policy `json:"-"`
|
||||
|
||||
Owner string
|
||||
}
|
||||
|
||||
// Policy returns the MDM policy applied to this Config. Returns a non-nil
|
||||
@@ -712,6 +716,13 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
|
||||
updated = true
|
||||
}
|
||||
|
||||
if input.Owner != nil {
|
||||
ownerString := ipcauth.OwnerPrincipalForIdentity(*input.Owner)
|
||||
config.Owner = ownerString
|
||||
log.Infof("setting '%s' as owner for profile %s", ownerString, config.Name)
|
||||
updated = true
|
||||
}
|
||||
|
||||
// MDM is the last override layer: any key present in the policy
|
||||
// supersedes defaults, on-disk config, env vars and CLI input.
|
||||
config.applyMDMPolicy(loadMDMPolicy())
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -17,7 +18,7 @@ type testPrefsSection struct {
|
||||
|
||||
func TestProfilePrefs_RoundTrip(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, username string) {
|
||||
created, err := sm.AddProfile("work", username)
|
||||
created, err := sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
|
||||
prefs, err := sm.ProfilePrefs(created.ID, username)
|
||||
@@ -42,7 +43,7 @@ func TestProfilePrefs_RoundTrip(t *testing.T) {
|
||||
|
||||
func TestProfilePrefs_GetMissingNamespace(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, username string) {
|
||||
created, err := sm.AddProfile("work", username)
|
||||
created, err := sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
|
||||
prefs, err := sm.ProfilePrefs(created.ID, username)
|
||||
@@ -57,7 +58,7 @@ func TestProfilePrefs_GetMissingNamespace(t *testing.T) {
|
||||
|
||||
func TestProfilePrefs_RemoveNamespace(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, username string) {
|
||||
created, err := sm.AddProfile("work", username)
|
||||
created, err := sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
|
||||
prefs, err := sm.ProfilePrefs(created.ID, username)
|
||||
@@ -90,7 +91,7 @@ func TestProfilePrefs_RejectsInvalidID(t *testing.T) {
|
||||
|
||||
func TestProfilePrefs_RejectsEmptyNamespace(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, username string) {
|
||||
created, err := sm.AddProfile("work", username)
|
||||
created, err := sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
|
||||
prefs, err := sm.ProfilePrefs(created.ID, username)
|
||||
@@ -118,7 +119,7 @@ func TestProfilePrefs_DefaultProfile(t *testing.T) {
|
||||
|
||||
func TestRemoveProfile_DeletesPrefsFile(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, username string) {
|
||||
created, err := sm.AddProfile("work", username)
|
||||
created, err := sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
|
||||
prefs, err := sm.ProfilePrefs(created.ID, username)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
)
|
||||
|
||||
@@ -53,6 +54,10 @@ type profileMeta struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type ownerMeta struct {
|
||||
Owner string
|
||||
}
|
||||
|
||||
func (e *ErrAmbiguousHandle) Error() string {
|
||||
switch e.Kind {
|
||||
case AmbiguityKindIDPrefix:
|
||||
@@ -296,7 +301,7 @@ func (s *ServiceManager) DefaultProfilePath() string {
|
||||
// The returned Profile carries the freshly-generated ID so callers can
|
||||
// show it to the user (and so the gRPC AddProfileResponse can include
|
||||
// it).
|
||||
func (s *ServiceManager) AddProfile(displayName, username string) (*Profile, error) {
|
||||
func (s *ServiceManager) AddProfile(displayName string, username string, callerId ipcauth.Identity) (*Profile, error) {
|
||||
configDir, err := s.getConfigDir(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config directory: %w", err)
|
||||
@@ -313,7 +318,7 @@ func (s *ServiceManager) AddProfile(displayName, username string) (*Profile, err
|
||||
}
|
||||
|
||||
profPath := filepath.Join(configDir, id.String()+".json")
|
||||
cfg, err := createNewConfig(ConfigInput{ConfigPath: profPath})
|
||||
cfg, err := createNewConfig(ConfigInput{ConfigPath: profPath, Owner: &callerId})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new config: %w", err)
|
||||
}
|
||||
@@ -571,6 +576,35 @@ func readProfileName(path string) string {
|
||||
return meta.Name
|
||||
}
|
||||
|
||||
func readProfileOwner(path string) string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var meta ownerMeta
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
return ""
|
||||
}
|
||||
return meta.Owner
|
||||
}
|
||||
|
||||
func stampOwner(path string, owner ipcauth.Identity) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.Owner = ipcauth.OwnerPrincipalForIdentity(owner)
|
||||
|
||||
if err := util.WriteJson(context.Background(), path, cfg); err != nil {
|
||||
return fmt.Errorf("failed to write profile owner: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// activeProfileID returns the currently-active profile's ID. The second
|
||||
// return value is true when the active profile is the default one.
|
||||
func (s *ServiceManager) activeProfileID() (ID, bool) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
)
|
||||
|
||||
@@ -32,7 +33,7 @@ func withTestSM(t *testing.T, fn func(sm *ServiceManager, username string)) {
|
||||
|
||||
func TestServiceProfile_ExactID(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, username string) {
|
||||
created, err := sm.AddProfile("work", username)
|
||||
created, err := sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := sm.ResolveProfile(created.ID.String(), username)
|
||||
@@ -44,7 +45,7 @@ func TestServiceProfile_ExactID(t *testing.T) {
|
||||
|
||||
func TestServiceProfile_IDPrefix(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, username string) {
|
||||
created, err := sm.AddProfile("work", username)
|
||||
created, err := sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
|
||||
prefix := created.ID[:4]
|
||||
@@ -75,7 +76,7 @@ func TestServiceProfile_AmbiguousPrefix(t *testing.T) {
|
||||
|
||||
func TestServiceProfile_ExactNameUnique(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, username string) {
|
||||
_, err := sm.AddProfile("work", username)
|
||||
_, err := sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := sm.ResolveProfile("work", username)
|
||||
@@ -86,9 +87,9 @@ func TestServiceProfile_ExactNameUnique(t *testing.T) {
|
||||
|
||||
func TestServiceProfile_AmbiguousName(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, username string) {
|
||||
_, err := sm.AddProfile("work", username)
|
||||
_, err := sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
_, err = sm.AddProfile("work", username)
|
||||
_, err = sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = sm.ResolveProfile("work", username)
|
||||
@@ -133,10 +134,10 @@ func TestServiceProfile_LegacyFilenameCoexists(t *testing.T) {
|
||||
|
||||
func TestAddProfile_AllowsDuplicateWithFlag(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, username string) {
|
||||
first, err := sm.AddProfile("work", username)
|
||||
first, err := sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
|
||||
second, err := sm.AddProfile("work", username)
|
||||
second, err := sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, first.ID, second.ID)
|
||||
assert.Equal(t, "work", second.Name)
|
||||
@@ -151,7 +152,7 @@ func TestAddProfile_RejectsInvalidNames(t *testing.T) {
|
||||
strings.Repeat("a", maxProfileNameLen+1), // too long
|
||||
}
|
||||
for _, name := range cases {
|
||||
_, err := sm.AddProfile(name, username)
|
||||
_, err := sm.AddProfile(name, username, ipcauth.Identity{})
|
||||
assert.Error(t, err, "expected error for %q", name)
|
||||
}
|
||||
})
|
||||
@@ -215,7 +216,7 @@ func TestIsValidProfileFilenameStem(t *testing.T) {
|
||||
|
||||
func TestRemoveProfile_DeletesStateFile(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, username string) {
|
||||
created, err := sm.AddProfile("work", username)
|
||||
created, err := sm.AddProfile("work", username, ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
|
||||
configDir, err := sm.getConfigDir(username)
|
||||
|
||||
@@ -2286,7 +2286,11 @@ 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)
|
||||
callerId, ok := ipcauth.CallerIdentity(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to get identity from context")
|
||||
}
|
||||
created, err := s.profileManager.AddProfile(msg.ProfileName, msg.Username, callerId)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create profile: %v", err)
|
||||
return nil, fmt.Errorf("failed to create profile: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user