Add WIP profile ownership

This commit is contained in:
Theodor S. Midtlien
2026-09-03 11:50:21 +02:00
parent 34f78b5119
commit 1f262ba044
6 changed files with 116 additions and 17 deletions
+48
View File
@@ -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)
}
+11
View File
@@ -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 -5
View File
@@ -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)
+36 -2
View File
@@ -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) {
+10 -9
View File
@@ -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)
+5 -1
View File
@@ -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)