mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-16 19:59:07 +02:00
Refactor owner to be daemon-wide
This commit is contained in:
+16
-15
@@ -13,18 +13,19 @@ import (
|
||||
|
||||
var ownerCmd = &cobra.Command{
|
||||
Use: "owner",
|
||||
Short: "Manage who may control the active NetBird profile",
|
||||
Long: `Manage the owners of the active profile's daemon control channel.
|
||||
Short: "Manage who may control the NetBird daemon",
|
||||
Long: `Manage the daemon-wide owners.
|
||||
|
||||
Ownership is enforced per profile: an isolated profile can only be controlled by
|
||||
its owner principals (plus root/administrator). A new profile is automatically
|
||||
owned by its creator; an unowned profile is claimed by the first caller.`,
|
||||
Owners are enforced on the daemon and stored in the service parameters. All owners
|
||||
may control the daemon and use the shared default profile (plus root/administrator),
|
||||
every other profile stays isolated to the user that created it. An unowned daemon
|
||||
is claimed by the first caller (trust-on-first-use).`,
|
||||
}
|
||||
|
||||
var ownerAddCmd = &cobra.Command{
|
||||
Use: "add <principal>",
|
||||
Short: "Add an owner principal to the active profile",
|
||||
Long: `Add an owner principal to the active profile. Principals are typed:
|
||||
Short: "Add a daemon owner principal",
|
||||
Long: `Add a daemon-wide owner principal. Principals are typed:
|
||||
uid:1000 a Unix user ID
|
||||
gid:1000 a Unix group ID
|
||||
group:netbird-admins a Unix group name (resolved via NSS/getent)
|
||||
@@ -37,7 +38,7 @@ Requires root/administrator or an existing owner.`,
|
||||
if _, err := c.AddOwner(ctx, &proto.AddOwnerRequest{Principal: args[0]}); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Printf("Added owner %q to the active profile\n", args[0])
|
||||
cmd.Printf("Added daemon owner %q\n", args[0])
|
||||
return nil
|
||||
})
|
||||
},
|
||||
@@ -45,8 +46,8 @@ Requires root/administrator or an existing owner.`,
|
||||
|
||||
var ownerResetCmd = &cobra.Command{
|
||||
Use: "reset",
|
||||
Short: "Clear the active profile's owner list (root/administrator only)",
|
||||
Long: `Clear the active profile's owner list, returning it to the unowned
|
||||
Short: "Clear the daemon owner list (root/administrator only)",
|
||||
Long: `Clear the daemon-wide owner list, returning the daemon to the unowned
|
||||
state. The next caller then claims ownership (trust-on-first-use). Requires
|
||||
root/administrator.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -54,7 +55,7 @@ root/administrator.`,
|
||||
if _, err := c.ResetOwner(ctx, &proto.ResetOwnerRequest{}); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Println("Owner list cleared; the next caller will claim ownership")
|
||||
cmd.Println("Daemon owner list cleared, the next caller will claim ownership")
|
||||
return nil
|
||||
})
|
||||
},
|
||||
@@ -62,13 +63,13 @@ root/administrator.`,
|
||||
|
||||
var ownerShareCmd = &cobra.Command{
|
||||
Use: "share",
|
||||
Short: "Mark the active profile shared (any local user may control it)",
|
||||
Short: "Mark the daemon shared (any local user may control it)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return withDaemon(cmd, func(ctx context.Context, c proto.DaemonServiceClient) error {
|
||||
if _, err := c.ShareProfile(ctx, &proto.ShareProfileRequest{Shared: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Println("Active profile is now shared with all local users")
|
||||
cmd.Println("Daemon is now shared with all local users")
|
||||
return nil
|
||||
})
|
||||
},
|
||||
@@ -76,13 +77,13 @@ var ownerShareCmd = &cobra.Command{
|
||||
|
||||
var ownerUnshareCmd = &cobra.Command{
|
||||
Use: "unshare",
|
||||
Short: "Stop sharing the active profile (restrict to its owners)",
|
||||
Short: "Stop sharing the daemon (restrict to its owners)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return withDaemon(cmd, func(ctx context.Context, c proto.DaemonServiceClient) error {
|
||||
if _, err := c.ShareProfile(ctx, &proto.ShareProfileRequest{Shared: false}); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Println("Active profile is no longer shared")
|
||||
cmd.Println("Daemon is no longer shared")
|
||||
return nil
|
||||
})
|
||||
},
|
||||
|
||||
@@ -30,6 +30,12 @@ var (
|
||||
serviceEnvVars []string
|
||||
jsonSocket string
|
||||
enableJSONSocket bool
|
||||
// owners seeds the daemon-wide owner set at install time (--owner). At runtime
|
||||
// the daemon reads and writes owners in service.json directly.
|
||||
owners []string
|
||||
// daemonShared carries the persisted daemon shared flag across
|
||||
// install/reconfigure round-trips (set at runtime via `netbird owner share`).
|
||||
daemonShared bool
|
||||
)
|
||||
|
||||
type program struct {
|
||||
@@ -55,6 +61,7 @@ func init() {
|
||||
serviceCmd.PersistentFlags().BoolVar(&networksDisabled, "disable-networks", false, "Disables network selection. If enabled, the client will not allow listing, selecting, or deselecting networks. To persist, use: netbird service install --disable-networks")
|
||||
serviceCmd.PersistentFlags().BoolVar(&enableJSONSocket, "enable-json-socket", false, "Enables the HTTP/JSON API socket served by grpc-gateway. To persist, use: netbird service install --enable-json-socket")
|
||||
serviceCmd.PersistentFlags().StringVar(&jsonSocket, "json-socket", defaultJSONSocket, "HTTP/JSON API socket address [unix|tcp|npipe]://[path|host:port|name]. Requires --enable-json-socket to serve. To persist, use: netbird service install --enable-json-socket --json-socket")
|
||||
serviceCmd.PersistentFlags().StringSliceVar(&owners, "owner", nil, "Principal(s) allowed to control the daemon and its default profile: uid:1000, gid:1000, group:netbird-admins (NSS), or sid:S-1-5-... (Windows). Repeatable. Other profiles stay isolated per user. To persist: netbird service install --owner uid:1000")
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
|
||||
serviceEnvDesc := `Sets extra environment variables for the service. ` +
|
||||
|
||||
@@ -115,6 +115,9 @@ func (p *program) Start(svc service.Service) error {
|
||||
}
|
||||
|
||||
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, captureEnabled, networksDisabled)
|
||||
// Daemon-wide owners live in service.json (governs the default profile and
|
||||
// daemon access), wire persistence before serving so owner add / TOFU work.
|
||||
serverInstance.SetDaemonOwnerStore(daemonOwnerStore{})
|
||||
if err := serverInstance.Start(); err != nil {
|
||||
log.Fatalf("failed to start daemon: %v", err)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,15 @@ type serviceParams struct {
|
||||
DisableNetworks bool `json:"disable_networks,omitempty"`
|
||||
EnableJSONSocket bool `json:"enable_json_socket,omitempty"`
|
||||
ServiceEnvVars map[string]string `json:"service_env_vars,omitempty"`
|
||||
// Owners lists the principals allowed to control this profile over the local
|
||||
// IPC, as typed strings: "uid:1000", "gid:1000", "group:netbird-admins"
|
||||
// (Unix, NSS-resolved) or "sid:S-1-5-..." (Windows user or group SID). Empty
|
||||
// with Shared=false means the profile is owned by nobody yet, until claimed
|
||||
Owners []string `json:"owners,omitempty"`
|
||||
|
||||
// Shared, when true, lets any authenticated local caller control this profile
|
||||
// (opt-in). Takes precedence over Owners.
|
||||
Shared bool `json:"shared,omitempty"`
|
||||
}
|
||||
|
||||
// serviceParamsPath returns the path to the service params file.
|
||||
@@ -40,6 +49,38 @@ func serviceParamsPath() string {
|
||||
return filepath.Join(configs.StateDir, serviceParamsFile)
|
||||
}
|
||||
|
||||
// daemonOwnerStore persists the daemon-wide owner set in service.json. It
|
||||
// implements server.DaemonOwnerStore so the daemon can read owners at startup and
|
||||
// mutate them at runtime (owner add, reset, share, TOFU claim) without server
|
||||
// importing cmd. Load-modify-write preserves the other service.json fields.
|
||||
type daemonOwnerStore struct{}
|
||||
|
||||
func (daemonOwnerStore) Load() ([]string, bool, error) {
|
||||
params, err := loadServiceParams()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if params == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
return params.Owners, params.Shared, nil
|
||||
}
|
||||
|
||||
func (daemonOwnerStore) Save(owners []string, shared bool) error {
|
||||
params, err := loadServiceParams()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if params == nil {
|
||||
// No service.json yet (daemon started without `service install`). Seed it
|
||||
// from the running daemon's current parameters so the file stays complete.
|
||||
params = currentServiceParams()
|
||||
}
|
||||
params.Owners = owners
|
||||
params.Shared = shared
|
||||
return saveServiceParams(params)
|
||||
}
|
||||
|
||||
// loadServiceParams reads saved service parameters from disk.
|
||||
// Returns nil with no error if the file does not exist.
|
||||
func loadServiceParams() (*serviceParams, error) {
|
||||
@@ -86,6 +127,8 @@ func currentServiceParams() *serviceParams {
|
||||
EnableCapture: captureEnabled,
|
||||
DisableNetworks: networksDisabled,
|
||||
EnableJSONSocket: enableJSONSocket,
|
||||
Owners: owners,
|
||||
Shared: daemonShared,
|
||||
}
|
||||
|
||||
if len(serviceEnvVars) > 0 {
|
||||
@@ -169,6 +212,14 @@ func applyServiceParams(cmd *cobra.Command, params *serviceParams) {
|
||||
networksDisabled = params.DisableNetworks
|
||||
}
|
||||
|
||||
// Carry the daemon-wide owner set forward across install/reconfigure so a
|
||||
// runtime owner add or TOFU claim in service.json is not clobbered. --owner
|
||||
// overrides.
|
||||
if !serviceCmd.PersistentFlags().Changed("owner") && len(params.Owners) > 0 {
|
||||
owners = params.Owners
|
||||
}
|
||||
daemonShared = params.Shared
|
||||
|
||||
applyServiceEnvParams(cmd, params)
|
||||
}
|
||||
|
||||
|
||||
@@ -431,9 +431,15 @@ func TestServiceParams_BuildArgsCoversAllFlags(t *testing.T) {
|
||||
installerFile, err := parser.ParseFile(fset, "service_installer.go", nil, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Fields that are handled outside of buildServiceArguments (env vars go through newSVCConfig).
|
||||
// Fields that are handled outside of buildServiceArguments.
|
||||
// - ServiceEnvVars flows through newSVCConfig() EnvVars, not CLI args.
|
||||
// - Owners/Shared are daemon-wide ownership persisted in service.json and
|
||||
// read+mutated by the daemon at runtime (owner add / TOFU claim); they are
|
||||
// deliberately NOT baked into the run args so runtime changes are not lost.
|
||||
fieldsNotInArgs := map[string]bool{
|
||||
"ServiceEnvVars": true,
|
||||
"Owners": true,
|
||||
"Shared": true,
|
||||
}
|
||||
|
||||
buildFields := extractFuncGlobalRefs(t, installerFile, "buildServiceArguments")
|
||||
|
||||
Reference in New Issue
Block a user