From 1d88e71c8b6054f7b2fbc7d056438c162ea00556 Mon Sep 17 00:00:00 2001 From: Paul Querna Date: Wed, 1 Jul 2026 19:14:54 +0000 Subject: [PATCH 1/4] Add managed device configuration and default device-flow login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `pkg/managedconfig` package that reads the ConductorOne managed device configuration — administrator-defined policy delivered to a device through an MDM — from the company-level `ai.c1` managed store. This lets a client auto-discover the tenant it belongs to without per-machine manual setup. Per-OS backends (read-only): - Linux: /etc/c1/managed.toml (TOML) - macOS: the `ai.c1` managed-preferences domain (via `defaults`) - Windows: HKLM\SOFTWARE\Policies\ConductorOne\C1 (REG_SZ) The store is read as a key/value map: unknown keys are ignored, and an absent, unreadable, or malformed store yields a zero Config. The package never returns an error or panics, so callers can consult it unconditionally. The single v1 key, `TenantDomain`, is the full DNS host of the tenant's control plane (e.g. `acme.conductor.one`); `ControlPlaneURL()` derives `https://{TenantDomain}` from it. Wire `cone login` to consult the managed configuration first: a bare `cone login` with no argument now discovers its tenant from managed policy and goes straight into the existing OAuth 2.0 Device Authorization Grant flow. When no managed configuration is present, behavior is unchanged and the tenant must be supplied as an argument. Co-authored-by: c1-squire-dev[bot] --- cmd/cone/login.go | 32 +- go.mod | 4 +- pkg/managedconfig/managedconfig.go | 113 +++++ pkg/managedconfig/managedconfig_darwin.go | 23 ++ pkg/managedconfig/managedconfig_linux.go | 19 + pkg/managedconfig/managedconfig_linux_test.go | 70 ++++ pkg/managedconfig/managedconfig_other.go | 9 + pkg/managedconfig/managedconfig_test.go | 97 +++++ pkg/managedconfig/managedconfig_windows.go | 24 ++ .../golang.org/x/sys/windows/registry/key.go | 214 ++++++++++ .../x/sys/windows/registry/mksyscall.go | 9 + .../x/sys/windows/registry/syscall.go | 32 ++ .../x/sys/windows/registry/value.go | 390 ++++++++++++++++++ .../sys/windows/registry/zsyscall_windows.go | 117 ++++++ vendor/modules.txt | 1 + 15 files changed, 1148 insertions(+), 6 deletions(-) create mode 100644 pkg/managedconfig/managedconfig.go create mode 100644 pkg/managedconfig/managedconfig_darwin.go create mode 100644 pkg/managedconfig/managedconfig_linux.go create mode 100644 pkg/managedconfig/managedconfig_linux_test.go create mode 100644 pkg/managedconfig/managedconfig_other.go create mode 100644 pkg/managedconfig/managedconfig_test.go create mode 100644 pkg/managedconfig/managedconfig_windows.go create mode 100644 vendor/golang.org/x/sys/windows/registry/key.go create mode 100644 vendor/golang.org/x/sys/windows/registry/mksyscall.go create mode 100644 vendor/golang.org/x/sys/windows/registry/syscall.go create mode 100644 vendor/golang.org/x/sys/windows/registry/value.go create mode 100644 vendor/golang.org/x/sys/windows/registry/zsyscall_windows.go diff --git a/cmd/cone/login.go b/cmd/cone/login.go index 0d2ed7fb..cf237564 100644 --- a/cmd/cone/login.go +++ b/cmd/cone/login.go @@ -13,27 +13,51 @@ import ( conductoroneapi "github.com/conductorone/conductorone-sdk-go" "github.com/conductorone/cone/pkg/client" + "github.com/conductorone/cone/pkg/managedconfig" ) func loginCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "login ", + Use: "login [tenant-name or tenant-url]", Short: fmt.Sprintf("Authenticate to ConductorOne, creating config.yaml in %s if it doesn't exist.", defaultConfigPath()), - RunE: loginRun, + Long: fmt.Sprintf("Authenticate to ConductorOne, creating config.yaml in %s if it doesn't exist.\n\n"+ + "If a managed device configuration is present, the tenant is discovered from it automatically "+ + "and the tenant argument may be omitted.", defaultConfigPath()), + RunE: loginRun, } cmd.Flags().String("profile", "default", "Config profile to create or update.") return cmd } +// resolveLoginTenant determines the tenant (name or URL) to authenticate +// against. Managed device configuration pushed by an administrator takes +// precedence over an argument supplied on the command line, allowing a bare +// "cone login" to discover its tenant automatically. When no managed +// configuration is present the behavior is unchanged: the tenant must be passed +// as an argument. The returned bool reports whether the tenant was sourced from +// managed configuration. +func resolveLoginTenant(cmd *cobra.Command, args []string) (string, bool, error) { + if serverURL := managedconfig.Read().ControlPlaneURL(); serverURL != "" { + return serverURL, true, nil + } + if err := validateArgLenth(1, args, cmd); err != nil { + return "", false, err + } + return args[0], false, nil +} + func loginRun(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - if err := validateArgLenth(1, args, cmd); err != nil { + tenant, fromManaged, err := resolveLoginTenant(cmd, args) + if err != nil { return err } - tenant := args[0] + if fromManaged { + pterm.Info.Printfln("Using tenant %q from managed device configuration.", tenant) + } spinner, err := pterm.DefaultSpinner.Start("Logging in...") if err != nil { diff --git a/go.mod b/go.mod index 1fa49940..4a56637b 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 github.com/pquerna/xjwt v0.3.0 github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.9.2 // indirect @@ -56,7 +56,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b golang.org/x/oauth2 v0.30.0 - golang.org/x/sys v0.33.0 // indirect + golang.org/x/sys v0.33.0 golang.org/x/text v0.26.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/pkg/managedconfig/managedconfig.go b/pkg/managedconfig/managedconfig.go new file mode 100644 index 00000000..10f125b0 --- /dev/null +++ b/pkg/managedconfig/managedconfig.go @@ -0,0 +1,113 @@ +// Package managedconfig reads the ConductorOne managed device configuration: +// administrator-defined policy delivered to a device through an MDM. It lets any +// ConductorOne client auto-discover the tenant it belongs to without per-machine +// manual setup. +// +// The configuration lives in a company-level, read-only managed store addressed +// by the namespace "ai.c1". Reads are best-effort by design: an absent, +// unreadable, or malformed store yields a zero Config. Nothing in this package +// returns an error or panics, so callers can consult it unconditionally at the +// top of their configuration-resolution chain. +package managedconfig + +import ( + "strings" + + "github.com/pelletier/go-toml/v2" +) + +const ( + // Namespace is the managed-config store the client reads from. + Namespace = "ai.c1" + + // KeyTenantDomain is the key holding the full DNS host of the tenant's + // control plane, for example "acme.conductor.one". + KeyTenantDomain = "TenantDomain" +) + +// Config holds the managed device configuration values the client understands. +// Keys the client does not recognize are ignored. +type Config struct { + // TenantDomain is the full DNS host of the tenant's control plane, for + // example "acme.conductor.one". It is empty when unset or invalid. + TenantDomain string +} + +// ControlPlaneURL returns the tenant control-plane URL ("https://" + TenantDomain), +// or an empty string when no valid TenantDomain is configured. +func (c Config) ControlPlaneURL() string { + if c.TenantDomain == "" { + return "" + } + return "https://" + c.TenantDomain +} + +// Read returns the managed device configuration for the current operating +// system. It never returns an error: an absent, unreadable, or malformed store +// yields a zero Config. +func Read() Config { + return configFromMap(readManagedConfig()) +} + +// configFromMap extracts the recognized keys from a raw key/value view of the +// store, ignoring unknown keys and dropping values that do not satisfy the +// contract. +func configFromMap(m map[string]string) Config { + var c Config + if domain := strings.TrimSpace(m[KeyTenantDomain]); isValidTenantDomain(domain) { + c.TenantDomain = domain + } + return c +} + +// isValidTenantDomain reports whether s is a full DNS host suitable for use as a +// control-plane locator: at least three dot-separated labels (for example +// "acme.conductor.one" or "acme.eu.c1.ai") with no scheme, path, port, or +// whitespace. A bare tenant slug is intentionally rejected. +func isValidTenantDomain(s string) bool { + if strings.ContainsAny(s, "/:@ \t\r\n") { + return false + } + labels := strings.Split(s, ".") + if len(labels) < 3 { + return false + } + for _, l := range labels { + if l == "" { + return false + } + } + return true +} + +// parseManagedTOML parses the Linux managed-config TOML into a flat map of +// top-level string values. Nested tables and non-string values are ignored. +// Malformed input yields a nil map. +func parseManagedTOML(data []byte) map[string]string { + raw := map[string]any{} + if err := toml.Unmarshal(data, &raw); err != nil { + return nil + } + return stringValues(raw) +} + +// stringValues returns only the top-level string entries of raw. +func stringValues(raw map[string]any) map[string]string { + out := make(map[string]string, len(raw)) + for k, v := range raw { + if s, ok := v.(string); ok { + out[k] = s + } + } + return out +} + +// parseDefaultsValue converts the raw output of `defaults read ` +// into a single-key map. Empty output yields a nil map. +func parseDefaultsValue(key string, out []byte) map[string]string { + v := strings.TrimSpace(string(out)) + if v == "" { + return nil + } + return map[string]string{key: v} +} diff --git a/pkg/managedconfig/managedconfig_darwin.go b/pkg/managedconfig/managedconfig_darwin.go new file mode 100644 index 00000000..b77cc896 --- /dev/null +++ b/pkg/managedconfig/managedconfig_darwin.go @@ -0,0 +1,23 @@ +//go:build darwin + +package managedconfig + +import "os/exec" + +// defaultsRead reads a single managed-preferences value through the `defaults` +// tool. Reading via the managed-preferences layer (rather than the on-disk +// plist) ensures only administrator-pushed policy is honored. It is a variable +// so tests can stub the lookup. +var defaultsRead = func(domain, key string) ([]byte, error) { + return exec.Command("defaults", "read", domain, key).Output() +} + +// readManagedConfig reads the managed device configuration from the macOS +// managed-preferences domain. A missing key or lookup error yields a nil map. +func readManagedConfig() map[string]string { + out, err := defaultsRead(Namespace, KeyTenantDomain) + if err != nil { + return nil + } + return parseDefaultsValue(KeyTenantDomain, out) +} diff --git a/pkg/managedconfig/managedconfig_linux.go b/pkg/managedconfig/managedconfig_linux.go new file mode 100644 index 00000000..48a2ab86 --- /dev/null +++ b/pkg/managedconfig/managedconfig_linux.go @@ -0,0 +1,19 @@ +//go:build linux + +package managedconfig + +import "os" + +// linuxConfigPath is the location of the Linux managed device configuration +// file. It is a variable so tests can point it at a temporary file. +var linuxConfigPath = "/etc/c1/managed.toml" + +// readManagedConfig reads the managed device configuration from the Linux +// managed-config file. A missing or unreadable file yields a nil map. +func readManagedConfig() map[string]string { + data, err := os.ReadFile(linuxConfigPath) + if err != nil { + return nil + } + return parseManagedTOML(data) +} diff --git a/pkg/managedconfig/managedconfig_linux_test.go b/pkg/managedconfig/managedconfig_linux_test.go new file mode 100644 index 00000000..790bcc5d --- /dev/null +++ b/pkg/managedconfig/managedconfig_linux_test.go @@ -0,0 +1,70 @@ +//go:build linux + +package managedconfig + +import ( + "os" + "path/filepath" + "testing" +) + +// withLinuxConfigPath points the package at path for the duration of the test. +func withLinuxConfigPath(t *testing.T, path string) { + t.Helper() + prev := linuxConfigPath + linuxConfigPath = path + t.Cleanup(func() { linuxConfigPath = prev }) +} + +func writeTempConfig(t *testing.T, contents string) string { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "managed.toml") + if err := os.WriteFile(path, []byte(contents), 0600); err != nil { + t.Fatalf("writing temp config: %v", err) + } + return path +} + +func TestReadManagedConfigLinux_Parse(t *testing.T) { + withLinuxConfigPath(t, writeTempConfig(t, "TenantDomain = \"acme.conductor.one\"\n")) + + if got := Read().TenantDomain; got != "acme.conductor.one" { + t.Errorf("TenantDomain = %q, want %q", got, "acme.conductor.one") + } + if got := Read().ControlPlaneURL(); got != "https://acme.conductor.one" { + t.Errorf("ControlPlaneURL() = %q, want %q", got, "https://acme.conductor.one") + } +} + +func TestReadManagedConfigLinux_UnknownKeyIgnored(t *testing.T) { + withLinuxConfigPath(t, writeTempConfig(t, "Unknown = \"junk\"\nTenantDomain = \"acme.eu.c1.ai\"\nExtra = 42\n")) + + if got := Read().TenantDomain; got != "acme.eu.c1.ai" { + t.Errorf("TenantDomain = %q, want %q", got, "acme.eu.c1.ai") + } +} + +func TestReadManagedConfigLinux_Absent(t *testing.T) { + withLinuxConfigPath(t, filepath.Join(t.TempDir(), "does-not-exist.toml")) + + if got := Read(); got != (Config{}) { + t.Errorf("Read() = %+v, want zero Config", got) + } +} + +func TestReadManagedConfigLinux_Malformed(t *testing.T) { + withLinuxConfigPath(t, writeTempConfig(t, "this is not = = valid toml")) + + if got := Read(); got != (Config{}) { + t.Errorf("Read() = %+v, want zero Config", got) + } +} + +func TestReadManagedConfigLinux_BareSlugRejected(t *testing.T) { + withLinuxConfigPath(t, writeTempConfig(t, "TenantDomain = \"acme\"\n")) + + if got := Read().TenantDomain; got != "" { + t.Errorf("TenantDomain = %q, want empty (bare slug should be rejected)", got) + } +} diff --git a/pkg/managedconfig/managedconfig_other.go b/pkg/managedconfig/managedconfig_other.go new file mode 100644 index 00000000..64e9b7b8 --- /dev/null +++ b/pkg/managedconfig/managedconfig_other.go @@ -0,0 +1,9 @@ +//go:build !linux && !darwin && !windows + +package managedconfig + +// readManagedConfig returns no configuration on platforms without a managed +// device configuration store. +func readManagedConfig() map[string]string { + return nil +} diff --git a/pkg/managedconfig/managedconfig_test.go b/pkg/managedconfig/managedconfig_test.go new file mode 100644 index 00000000..725b8e0a --- /dev/null +++ b/pkg/managedconfig/managedconfig_test.go @@ -0,0 +1,97 @@ +package managedconfig + +import "testing" + +func TestConfigControlPlaneURL(t *testing.T) { + tests := []struct { + name string + config Config + want string + }{ + {name: "empty", config: Config{}, want: ""}, + {name: "commercial", config: Config{TenantDomain: "acme.conductor.one"}, want: "https://acme.conductor.one"}, + {name: "eu", config: Config{TenantDomain: "acme.eu.c1.ai"}, want: "https://acme.eu.c1.ai"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.config.ControlPlaneURL(); got != tt.want { + t.Errorf("ControlPlaneURL() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestConfigFromMap(t *testing.T) { + tests := []struct { + name string + in map[string]string + want string + }{ + {name: "nil map", in: nil, want: ""}, + {name: "empty map", in: map[string]string{}, want: ""}, + {name: "valid commercial", in: map[string]string{KeyTenantDomain: "acme.conductor.one"}, want: "acme.conductor.one"}, + {name: "valid eu", in: map[string]string{KeyTenantDomain: "acme.eu.c1.ai"}, want: "acme.eu.c1.ai"}, + {name: "trims whitespace", in: map[string]string{KeyTenantDomain: " acme.conductor.one\n"}, want: "acme.conductor.one"}, + {name: "unknown keys ignored", in: map[string]string{"SomethingElse": "value", KeyTenantDomain: "acme.conductor.one"}, want: "acme.conductor.one"}, + {name: "bare slug rejected", in: map[string]string{KeyTenantDomain: "acme"}, want: ""}, + {name: "two-label rejected", in: map[string]string{KeyTenantDomain: "conductor.one"}, want: ""}, + {name: "scheme rejected", in: map[string]string{KeyTenantDomain: "https://acme.conductor.one"}, want: ""}, + {name: "path rejected", in: map[string]string{KeyTenantDomain: "acme.conductor.one/foo"}, want: ""}, + {name: "empty value", in: map[string]string{KeyTenantDomain: ""}, want: ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := configFromMap(tt.in).TenantDomain; got != tt.want { + t.Errorf("configFromMap(%v).TenantDomain = %q, want %q", tt.in, got, tt.want) + } + }) + } +} + +func TestIsValidTenantDomain(t *testing.T) { + valid := []string{"acme.conductor.one", "acme.eu.c1.ai", "a.b.c"} + for _, s := range valid { + if !isValidTenantDomain(s) { + t.Errorf("isValidTenantDomain(%q) = false, want true", s) + } + } + invalid := []string{"", "acme", "conductor.one", "acme.conductor.one/x", "https://acme.conductor.one", "acme.conductor.one:8080", "acme..one", "acme conductor one"} + for _, s := range invalid { + if isValidTenantDomain(s) { + t.Errorf("isValidTenantDomain(%q) = true, want false", s) + } + } +} + +func TestParseManagedTOML(t *testing.T) { + tests := []struct { + name string + in string + want string // expected TenantDomain after configFromMap; "" means absent/ignored + }{ + {name: "valid", in: "TenantDomain = \"acme.conductor.one\"\n", want: "acme.conductor.one"}, + {name: "unknown keys ignored", in: "TenantDomain = \"acme.conductor.one\"\nUnknown = \"x\"\nCount = 3\n", want: "acme.conductor.one"}, + {name: "absent key", in: "SomethingElse = \"x\"\n", want: ""}, + {name: "empty file", in: "", want: ""}, + {name: "malformed", in: "TenantDomain = = = broken", want: ""}, + {name: "non-string value ignored", in: "TenantDomain = 123\n", want: ""}, + {name: "nested table ignored", in: "[section]\nTenantDomain = \"acme.conductor.one\"\n", want: ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := configFromMap(parseManagedTOML([]byte(tt.in))).TenantDomain + if got != tt.want { + t.Errorf("parseManagedTOML(%q) TenantDomain = %q, want %q", tt.in, got, tt.want) + } + }) + } +} + +func TestParseDefaultsValue(t *testing.T) { + if m := parseDefaultsValue(KeyTenantDomain, []byte("acme.conductor.one\n")); m[KeyTenantDomain] != "acme.conductor.one" { + t.Errorf("parseDefaultsValue got %v", m) + } + if m := parseDefaultsValue(KeyTenantDomain, []byte(" \n")); m != nil { + t.Errorf("parseDefaultsValue(empty) = %v, want nil", m) + } +} diff --git a/pkg/managedconfig/managedconfig_windows.go b/pkg/managedconfig/managedconfig_windows.go new file mode 100644 index 00000000..aeadfba6 --- /dev/null +++ b/pkg/managedconfig/managedconfig_windows.go @@ -0,0 +1,24 @@ +//go:build windows + +package managedconfig + +import "golang.org/x/sys/windows/registry" + +// registryPath is the HKLM policy key holding the managed device configuration. +const registryPath = `SOFTWARE\Policies\ConductorOne\C1` + +// readManagedConfig reads the managed device configuration from the Windows +// registry policy key. A missing key or read error yields a nil map. +func readManagedConfig() map[string]string { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, registryPath, registry.QUERY_VALUE) + if err != nil { + return nil + } + defer k.Close() + + val, _, err := k.GetStringValue(KeyTenantDomain) + if err != nil { + return nil + } + return map[string]string{KeyTenantDomain: val} +} diff --git a/vendor/golang.org/x/sys/windows/registry/key.go b/vendor/golang.org/x/sys/windows/registry/key.go new file mode 100644 index 00000000..39aeeb64 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/registry/key.go @@ -0,0 +1,214 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows + +// Package registry provides access to the Windows registry. +// +// Here is a simple example, opening a registry key and reading a string value from it. +// +// k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) +// if err != nil { +// log.Fatal(err) +// } +// defer k.Close() +// +// s, _, err := k.GetStringValue("SystemRoot") +// if err != nil { +// log.Fatal(err) +// } +// fmt.Printf("Windows system root is %q\n", s) +package registry + +import ( + "io" + "runtime" + "syscall" + "time" +) + +const ( + // Registry key security and access rights. + // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms724878.aspx + // for details. + ALL_ACCESS = 0xf003f + CREATE_LINK = 0x00020 + CREATE_SUB_KEY = 0x00004 + ENUMERATE_SUB_KEYS = 0x00008 + EXECUTE = 0x20019 + NOTIFY = 0x00010 + QUERY_VALUE = 0x00001 + READ = 0x20019 + SET_VALUE = 0x00002 + WOW64_32KEY = 0x00200 + WOW64_64KEY = 0x00100 + WRITE = 0x20006 +) + +// Key is a handle to an open Windows registry key. +// Keys can be obtained by calling OpenKey; there are +// also some predefined root keys such as CURRENT_USER. +// Keys can be used directly in the Windows API. +type Key syscall.Handle + +const ( + // Windows defines some predefined root keys that are always open. + // An application can use these keys as entry points to the registry. + // Normally these keys are used in OpenKey to open new keys, + // but they can also be used anywhere a Key is required. + CLASSES_ROOT = Key(syscall.HKEY_CLASSES_ROOT) + CURRENT_USER = Key(syscall.HKEY_CURRENT_USER) + LOCAL_MACHINE = Key(syscall.HKEY_LOCAL_MACHINE) + USERS = Key(syscall.HKEY_USERS) + CURRENT_CONFIG = Key(syscall.HKEY_CURRENT_CONFIG) + PERFORMANCE_DATA = Key(syscall.HKEY_PERFORMANCE_DATA) +) + +// Close closes open key k. +func (k Key) Close() error { + return syscall.RegCloseKey(syscall.Handle(k)) +} + +// OpenKey opens a new key with path name relative to key k. +// It accepts any open key, including CURRENT_USER and others, +// and returns the new key and an error. +// The access parameter specifies desired access rights to the +// key to be opened. +func OpenKey(k Key, path string, access uint32) (Key, error) { + p, err := syscall.UTF16PtrFromString(path) + if err != nil { + return 0, err + } + var subkey syscall.Handle + err = syscall.RegOpenKeyEx(syscall.Handle(k), p, 0, access, &subkey) + if err != nil { + return 0, err + } + return Key(subkey), nil +} + +// OpenRemoteKey opens a predefined registry key on another +// computer pcname. The key to be opened is specified by k, but +// can only be one of LOCAL_MACHINE, PERFORMANCE_DATA or USERS. +// If pcname is "", OpenRemoteKey returns local computer key. +func OpenRemoteKey(pcname string, k Key) (Key, error) { + var err error + var p *uint16 + if pcname != "" { + p, err = syscall.UTF16PtrFromString(`\\` + pcname) + if err != nil { + return 0, err + } + } + var remoteKey syscall.Handle + err = regConnectRegistry(p, syscall.Handle(k), &remoteKey) + if err != nil { + return 0, err + } + return Key(remoteKey), nil +} + +// ReadSubKeyNames returns the names of subkeys of key k. +// The parameter n controls the number of returned names, +// analogous to the way os.File.Readdirnames works. +func (k Key) ReadSubKeyNames(n int) ([]string, error) { + // RegEnumKeyEx must be called repeatedly and to completion. + // During this time, this goroutine cannot migrate away from + // its current thread. See https://golang.org/issue/49320 and + // https://golang.org/issue/49466. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + names := make([]string, 0) + // Registry key size limit is 255 bytes and described there: + // https://msdn.microsoft.com/library/windows/desktop/ms724872.aspx + buf := make([]uint16, 256) //plus extra room for terminating zero byte +loopItems: + for i := uint32(0); ; i++ { + if n > 0 { + if len(names) == n { + return names, nil + } + } + l := uint32(len(buf)) + for { + err := syscall.RegEnumKeyEx(syscall.Handle(k), i, &buf[0], &l, nil, nil, nil, nil) + if err == nil { + break + } + if err == syscall.ERROR_MORE_DATA { + // Double buffer size and try again. + l = uint32(2 * len(buf)) + buf = make([]uint16, l) + continue + } + if err == _ERROR_NO_MORE_ITEMS { + break loopItems + } + return names, err + } + names = append(names, syscall.UTF16ToString(buf[:l])) + } + if n > len(names) { + return names, io.EOF + } + return names, nil +} + +// CreateKey creates a key named path under open key k. +// CreateKey returns the new key and a boolean flag that reports +// whether the key already existed. +// The access parameter specifies the access rights for the key +// to be created. +func CreateKey(k Key, path string, access uint32) (newk Key, openedExisting bool, err error) { + var h syscall.Handle + var d uint32 + var pathPointer *uint16 + pathPointer, err = syscall.UTF16PtrFromString(path) + if err != nil { + return 0, false, err + } + err = regCreateKeyEx(syscall.Handle(k), pathPointer, + 0, nil, _REG_OPTION_NON_VOLATILE, access, nil, &h, &d) + if err != nil { + return 0, false, err + } + return Key(h), d == _REG_OPENED_EXISTING_KEY, nil +} + +// DeleteKey deletes the subkey path of key k and its values. +func DeleteKey(k Key, path string) error { + pathPointer, err := syscall.UTF16PtrFromString(path) + if err != nil { + return err + } + return regDeleteKey(syscall.Handle(k), pathPointer) +} + +// A KeyInfo describes the statistics of a key. It is returned by Stat. +type KeyInfo struct { + SubKeyCount uint32 + MaxSubKeyLen uint32 // size of the key's subkey with the longest name, in Unicode characters, not including the terminating zero byte + ValueCount uint32 + MaxValueNameLen uint32 // size of the key's longest value name, in Unicode characters, not including the terminating zero byte + MaxValueLen uint32 // longest data component among the key's values, in bytes + lastWriteTime syscall.Filetime +} + +// ModTime returns the key's last write time. +func (ki *KeyInfo) ModTime() time.Time { + return time.Unix(0, ki.lastWriteTime.Nanoseconds()) +} + +// Stat retrieves information about the open key k. +func (k Key) Stat() (*KeyInfo, error) { + var ki KeyInfo + err := syscall.RegQueryInfoKey(syscall.Handle(k), nil, nil, nil, + &ki.SubKeyCount, &ki.MaxSubKeyLen, nil, &ki.ValueCount, + &ki.MaxValueNameLen, &ki.MaxValueLen, nil, &ki.lastWriteTime) + if err != nil { + return nil, err + } + return &ki, nil +} diff --git a/vendor/golang.org/x/sys/windows/registry/mksyscall.go b/vendor/golang.org/x/sys/windows/registry/mksyscall.go new file mode 100644 index 00000000..bbf86ccf --- /dev/null +++ b/vendor/golang.org/x/sys/windows/registry/mksyscall.go @@ -0,0 +1,9 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build generate + +package registry + +//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall.go diff --git a/vendor/golang.org/x/sys/windows/registry/syscall.go b/vendor/golang.org/x/sys/windows/registry/syscall.go new file mode 100644 index 00000000..f533091c --- /dev/null +++ b/vendor/golang.org/x/sys/windows/registry/syscall.go @@ -0,0 +1,32 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows + +package registry + +import "syscall" + +const ( + _REG_OPTION_NON_VOLATILE = 0 + + _REG_CREATED_NEW_KEY = 1 + _REG_OPENED_EXISTING_KEY = 2 + + _ERROR_NO_MORE_ITEMS syscall.Errno = 259 +) + +func LoadRegLoadMUIString() error { + return procRegLoadMUIStringW.Find() +} + +//sys regCreateKeyEx(key syscall.Handle, subkey *uint16, reserved uint32, class *uint16, options uint32, desired uint32, sa *syscall.SecurityAttributes, result *syscall.Handle, disposition *uint32) (regerrno error) = advapi32.RegCreateKeyExW +//sys regDeleteKey(key syscall.Handle, subkey *uint16) (regerrno error) = advapi32.RegDeleteKeyW +//sys regSetValueEx(key syscall.Handle, valueName *uint16, reserved uint32, vtype uint32, buf *byte, bufsize uint32) (regerrno error) = advapi32.RegSetValueExW +//sys regEnumValue(key syscall.Handle, index uint32, name *uint16, nameLen *uint32, reserved *uint32, valtype *uint32, buf *byte, buflen *uint32) (regerrno error) = advapi32.RegEnumValueW +//sys regDeleteValue(key syscall.Handle, name *uint16) (regerrno error) = advapi32.RegDeleteValueW +//sys regLoadMUIString(key syscall.Handle, name *uint16, buf *uint16, buflen uint32, buflenCopied *uint32, flags uint32, dir *uint16) (regerrno error) = advapi32.RegLoadMUIStringW +//sys regConnectRegistry(machinename *uint16, key syscall.Handle, result *syscall.Handle) (regerrno error) = advapi32.RegConnectRegistryW + +//sys expandEnvironmentStrings(src *uint16, dst *uint16, size uint32) (n uint32, err error) = kernel32.ExpandEnvironmentStringsW diff --git a/vendor/golang.org/x/sys/windows/registry/value.go b/vendor/golang.org/x/sys/windows/registry/value.go new file mode 100644 index 00000000..a1bcbb23 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/registry/value.go @@ -0,0 +1,390 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows + +package registry + +import ( + "errors" + "io" + "syscall" + "unicode/utf16" + "unsafe" +) + +const ( + // Registry value types. + NONE = 0 + SZ = 1 + EXPAND_SZ = 2 + BINARY = 3 + DWORD = 4 + DWORD_BIG_ENDIAN = 5 + LINK = 6 + MULTI_SZ = 7 + RESOURCE_LIST = 8 + FULL_RESOURCE_DESCRIPTOR = 9 + RESOURCE_REQUIREMENTS_LIST = 10 + QWORD = 11 +) + +var ( + // ErrShortBuffer is returned when the buffer was too short for the operation. + ErrShortBuffer = syscall.ERROR_MORE_DATA + + // ErrNotExist is returned when a registry key or value does not exist. + ErrNotExist = syscall.ERROR_FILE_NOT_FOUND + + // ErrUnexpectedType is returned by Get*Value when the value's type was unexpected. + ErrUnexpectedType = errors.New("unexpected key value type") +) + +// GetValue retrieves the type and data for the specified value associated +// with an open key k. It fills up buffer buf and returns the retrieved +// byte count n. If buf is too small to fit the stored value it returns +// ErrShortBuffer error along with the required buffer size n. +// If no buffer is provided, it returns true and actual buffer size n. +// If no buffer is provided, GetValue returns the value's type only. +// If the value does not exist, the error returned is ErrNotExist. +// +// GetValue is a low level function. If value's type is known, use the appropriate +// Get*Value function instead. +func (k Key) GetValue(name string, buf []byte) (n int, valtype uint32, err error) { + pname, err := syscall.UTF16PtrFromString(name) + if err != nil { + return 0, 0, err + } + var pbuf *byte + if len(buf) > 0 { + pbuf = (*byte)(unsafe.Pointer(&buf[0])) + } + l := uint32(len(buf)) + err = syscall.RegQueryValueEx(syscall.Handle(k), pname, nil, &valtype, pbuf, &l) + if err != nil { + return int(l), valtype, err + } + return int(l), valtype, nil +} + +func (k Key) getValue(name string, buf []byte) (data []byte, valtype uint32, err error) { + p, err := syscall.UTF16PtrFromString(name) + if err != nil { + return nil, 0, err + } + var t uint32 + n := uint32(len(buf)) + for { + err = syscall.RegQueryValueEx(syscall.Handle(k), p, nil, &t, (*byte)(unsafe.Pointer(&buf[0])), &n) + if err == nil { + return buf[:n], t, nil + } + if err != syscall.ERROR_MORE_DATA { + return nil, 0, err + } + if n <= uint32(len(buf)) { + return nil, 0, err + } + buf = make([]byte, n) + } +} + +// GetStringValue retrieves the string value for the specified +// value name associated with an open key k. It also returns the value's type. +// If value does not exist, GetStringValue returns ErrNotExist. +// If value is not SZ or EXPAND_SZ, it will return the correct value +// type and ErrUnexpectedType. +func (k Key) GetStringValue(name string) (val string, valtype uint32, err error) { + data, typ, err2 := k.getValue(name, make([]byte, 64)) + if err2 != nil { + return "", typ, err2 + } + switch typ { + case SZ, EXPAND_SZ: + default: + return "", typ, ErrUnexpectedType + } + if len(data) == 0 { + return "", typ, nil + } + u := (*[1 << 29]uint16)(unsafe.Pointer(&data[0]))[: len(data)/2 : len(data)/2] + return syscall.UTF16ToString(u), typ, nil +} + +// GetMUIStringValue retrieves the localized string value for +// the specified value name associated with an open key k. +// If the value name doesn't exist or the localized string value +// can't be resolved, GetMUIStringValue returns ErrNotExist. +// GetMUIStringValue panics if the system doesn't support +// regLoadMUIString; use LoadRegLoadMUIString to check if +// regLoadMUIString is supported before calling this function. +func (k Key) GetMUIStringValue(name string) (string, error) { + pname, err := syscall.UTF16PtrFromString(name) + if err != nil { + return "", err + } + + buf := make([]uint16, 1024) + var buflen uint32 + var pdir *uint16 + + err = regLoadMUIString(syscall.Handle(k), pname, &buf[0], uint32(len(buf)), &buflen, 0, pdir) + if err == syscall.ERROR_FILE_NOT_FOUND { // Try fallback path + + // Try to resolve the string value using the system directory as + // a DLL search path; this assumes the string value is of the form + // @[path]\dllname,-strID but with no path given, e.g. @tzres.dll,-320. + + // This approach works with tzres.dll but may have to be revised + // in the future to allow callers to provide custom search paths. + + var s string + s, err = ExpandString("%SystemRoot%\\system32\\") + if err != nil { + return "", err + } + pdir, err = syscall.UTF16PtrFromString(s) + if err != nil { + return "", err + } + + err = regLoadMUIString(syscall.Handle(k), pname, &buf[0], uint32(len(buf)), &buflen, 0, pdir) + } + + for err == syscall.ERROR_MORE_DATA { // Grow buffer if needed + if buflen <= uint32(len(buf)) { + break // Buffer not growing, assume race; break + } + buf = make([]uint16, buflen) + err = regLoadMUIString(syscall.Handle(k), pname, &buf[0], uint32(len(buf)), &buflen, 0, pdir) + } + + if err != nil { + return "", err + } + + return syscall.UTF16ToString(buf), nil +} + +// ExpandString expands environment-variable strings and replaces +// them with the values defined for the current user. +// Use ExpandString to expand EXPAND_SZ strings. +func ExpandString(value string) (string, error) { + if value == "" { + return "", nil + } + p, err := syscall.UTF16PtrFromString(value) + if err != nil { + return "", err + } + r := make([]uint16, 100) + for { + n, err := expandEnvironmentStrings(p, &r[0], uint32(len(r))) + if err != nil { + return "", err + } + if n <= uint32(len(r)) { + return syscall.UTF16ToString(r[:n]), nil + } + r = make([]uint16, n) + } +} + +// GetStringsValue retrieves the []string value for the specified +// value name associated with an open key k. It also returns the value's type. +// If value does not exist, GetStringsValue returns ErrNotExist. +// If value is not MULTI_SZ, it will return the correct value +// type and ErrUnexpectedType. +func (k Key) GetStringsValue(name string) (val []string, valtype uint32, err error) { + data, typ, err2 := k.getValue(name, make([]byte, 64)) + if err2 != nil { + return nil, typ, err2 + } + if typ != MULTI_SZ { + return nil, typ, ErrUnexpectedType + } + if len(data) == 0 { + return nil, typ, nil + } + p := (*[1 << 29]uint16)(unsafe.Pointer(&data[0]))[: len(data)/2 : len(data)/2] + if len(p) == 0 { + return nil, typ, nil + } + if p[len(p)-1] == 0 { + p = p[:len(p)-1] // remove terminating null + } + val = make([]string, 0, 5) + from := 0 + for i, c := range p { + if c == 0 { + val = append(val, string(utf16.Decode(p[from:i]))) + from = i + 1 + } + } + return val, typ, nil +} + +// GetIntegerValue retrieves the integer value for the specified +// value name associated with an open key k. It also returns the value's type. +// If value does not exist, GetIntegerValue returns ErrNotExist. +// If value is not DWORD or QWORD, it will return the correct value +// type and ErrUnexpectedType. +func (k Key) GetIntegerValue(name string) (val uint64, valtype uint32, err error) { + data, typ, err2 := k.getValue(name, make([]byte, 8)) + if err2 != nil { + return 0, typ, err2 + } + switch typ { + case DWORD: + if len(data) != 4 { + return 0, typ, errors.New("DWORD value is not 4 bytes long") + } + var val32 uint32 + copy((*[4]byte)(unsafe.Pointer(&val32))[:], data) + return uint64(val32), DWORD, nil + case QWORD: + if len(data) != 8 { + return 0, typ, errors.New("QWORD value is not 8 bytes long") + } + copy((*[8]byte)(unsafe.Pointer(&val))[:], data) + return val, QWORD, nil + default: + return 0, typ, ErrUnexpectedType + } +} + +// GetBinaryValue retrieves the binary value for the specified +// value name associated with an open key k. It also returns the value's type. +// If value does not exist, GetBinaryValue returns ErrNotExist. +// If value is not BINARY, it will return the correct value +// type and ErrUnexpectedType. +func (k Key) GetBinaryValue(name string) (val []byte, valtype uint32, err error) { + data, typ, err2 := k.getValue(name, make([]byte, 64)) + if err2 != nil { + return nil, typ, err2 + } + if typ != BINARY { + return nil, typ, ErrUnexpectedType + } + return data, typ, nil +} + +func (k Key) setValue(name string, valtype uint32, data []byte) error { + p, err := syscall.UTF16PtrFromString(name) + if err != nil { + return err + } + if len(data) == 0 { + return regSetValueEx(syscall.Handle(k), p, 0, valtype, nil, 0) + } + return regSetValueEx(syscall.Handle(k), p, 0, valtype, &data[0], uint32(len(data))) +} + +// SetDWordValue sets the data and type of a name value +// under key k to value and DWORD. +func (k Key) SetDWordValue(name string, value uint32) error { + return k.setValue(name, DWORD, (*[4]byte)(unsafe.Pointer(&value))[:]) +} + +// SetQWordValue sets the data and type of a name value +// under key k to value and QWORD. +func (k Key) SetQWordValue(name string, value uint64) error { + return k.setValue(name, QWORD, (*[8]byte)(unsafe.Pointer(&value))[:]) +} + +func (k Key) setStringValue(name string, valtype uint32, value string) error { + v, err := syscall.UTF16FromString(value) + if err != nil { + return err + } + buf := (*[1 << 29]byte)(unsafe.Pointer(&v[0]))[: len(v)*2 : len(v)*2] + return k.setValue(name, valtype, buf) +} + +// SetStringValue sets the data and type of a name value +// under key k to value and SZ. The value must not contain a zero byte. +func (k Key) SetStringValue(name, value string) error { + return k.setStringValue(name, SZ, value) +} + +// SetExpandStringValue sets the data and type of a name value +// under key k to value and EXPAND_SZ. The value must not contain a zero byte. +func (k Key) SetExpandStringValue(name, value string) error { + return k.setStringValue(name, EXPAND_SZ, value) +} + +// SetStringsValue sets the data and type of a name value +// under key k to value and MULTI_SZ. The value strings +// must not contain a zero byte. +func (k Key) SetStringsValue(name string, value []string) error { + ss := "" + for _, s := range value { + for i := 0; i < len(s); i++ { + if s[i] == 0 { + return errors.New("string cannot have 0 inside") + } + } + ss += s + "\x00" + } + v := utf16.Encode([]rune(ss + "\x00")) + buf := (*[1 << 29]byte)(unsafe.Pointer(&v[0]))[: len(v)*2 : len(v)*2] + return k.setValue(name, MULTI_SZ, buf) +} + +// SetBinaryValue sets the data and type of a name value +// under key k to value and BINARY. +func (k Key) SetBinaryValue(name string, value []byte) error { + return k.setValue(name, BINARY, value) +} + +// DeleteValue removes a named value from the key k. +func (k Key) DeleteValue(name string) error { + namePointer, err := syscall.UTF16PtrFromString(name) + if err != nil { + return err + } + return regDeleteValue(syscall.Handle(k), namePointer) +} + +// ReadValueNames returns the value names of key k. +// The parameter n controls the number of returned names, +// analogous to the way os.File.Readdirnames works. +func (k Key) ReadValueNames(n int) ([]string, error) { + ki, err := k.Stat() + if err != nil { + return nil, err + } + names := make([]string, 0, ki.ValueCount) + buf := make([]uint16, ki.MaxValueNameLen+1) // extra room for terminating null character +loopItems: + for i := uint32(0); ; i++ { + if n > 0 { + if len(names) == n { + return names, nil + } + } + l := uint32(len(buf)) + for { + err := regEnumValue(syscall.Handle(k), i, &buf[0], &l, nil, nil, nil, nil) + if err == nil { + break + } + if err == syscall.ERROR_MORE_DATA { + // Double buffer size and try again. + l = uint32(2 * len(buf)) + buf = make([]uint16, l) + continue + } + if err == _ERROR_NO_MORE_ITEMS { + break loopItems + } + return names, err + } + names = append(names, syscall.UTF16ToString(buf[:l])) + } + if n > len(names) { + return names, io.EOF + } + return names, nil +} diff --git a/vendor/golang.org/x/sys/windows/registry/zsyscall_windows.go b/vendor/golang.org/x/sys/windows/registry/zsyscall_windows.go new file mode 100644 index 00000000..fc1835d8 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/registry/zsyscall_windows.go @@ -0,0 +1,117 @@ +// Code generated by 'go generate'; DO NOT EDIT. + +package registry + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + + procRegConnectRegistryW = modadvapi32.NewProc("RegConnectRegistryW") + procRegCreateKeyExW = modadvapi32.NewProc("RegCreateKeyExW") + procRegDeleteKeyW = modadvapi32.NewProc("RegDeleteKeyW") + procRegDeleteValueW = modadvapi32.NewProc("RegDeleteValueW") + procRegEnumValueW = modadvapi32.NewProc("RegEnumValueW") + procRegLoadMUIStringW = modadvapi32.NewProc("RegLoadMUIStringW") + procRegSetValueExW = modadvapi32.NewProc("RegSetValueExW") + procExpandEnvironmentStringsW = modkernel32.NewProc("ExpandEnvironmentStringsW") +) + +func regConnectRegistry(machinename *uint16, key syscall.Handle, result *syscall.Handle) (regerrno error) { + r0, _, _ := syscall.Syscall(procRegConnectRegistryW.Addr(), 3, uintptr(unsafe.Pointer(machinename)), uintptr(key), uintptr(unsafe.Pointer(result))) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func regCreateKeyEx(key syscall.Handle, subkey *uint16, reserved uint32, class *uint16, options uint32, desired uint32, sa *syscall.SecurityAttributes, result *syscall.Handle, disposition *uint32) (regerrno error) { + r0, _, _ := syscall.Syscall9(procRegCreateKeyExW.Addr(), 9, uintptr(key), uintptr(unsafe.Pointer(subkey)), uintptr(reserved), uintptr(unsafe.Pointer(class)), uintptr(options), uintptr(desired), uintptr(unsafe.Pointer(sa)), uintptr(unsafe.Pointer(result)), uintptr(unsafe.Pointer(disposition))) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func regDeleteKey(key syscall.Handle, subkey *uint16) (regerrno error) { + r0, _, _ := syscall.Syscall(procRegDeleteKeyW.Addr(), 2, uintptr(key), uintptr(unsafe.Pointer(subkey)), 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func regDeleteValue(key syscall.Handle, name *uint16) (regerrno error) { + r0, _, _ := syscall.Syscall(procRegDeleteValueW.Addr(), 2, uintptr(key), uintptr(unsafe.Pointer(name)), 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func regEnumValue(key syscall.Handle, index uint32, name *uint16, nameLen *uint32, reserved *uint32, valtype *uint32, buf *byte, buflen *uint32) (regerrno error) { + r0, _, _ := syscall.Syscall9(procRegEnumValueW.Addr(), 8, uintptr(key), uintptr(index), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(nameLen)), uintptr(unsafe.Pointer(reserved)), uintptr(unsafe.Pointer(valtype)), uintptr(unsafe.Pointer(buf)), uintptr(unsafe.Pointer(buflen)), 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func regLoadMUIString(key syscall.Handle, name *uint16, buf *uint16, buflen uint32, buflenCopied *uint32, flags uint32, dir *uint16) (regerrno error) { + r0, _, _ := syscall.Syscall9(procRegLoadMUIStringW.Addr(), 7, uintptr(key), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(buf)), uintptr(buflen), uintptr(unsafe.Pointer(buflenCopied)), uintptr(flags), uintptr(unsafe.Pointer(dir)), 0, 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func regSetValueEx(key syscall.Handle, valueName *uint16, reserved uint32, vtype uint32, buf *byte, bufsize uint32) (regerrno error) { + r0, _, _ := syscall.Syscall6(procRegSetValueExW.Addr(), 6, uintptr(key), uintptr(unsafe.Pointer(valueName)), uintptr(reserved), uintptr(vtype), uintptr(unsafe.Pointer(buf)), uintptr(bufsize)) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func expandEnvironmentStrings(src *uint16, dst *uint16, size uint32) (n uint32, err error) { + r0, _, e1 := syscall.Syscall(procExpandEnvironmentStringsW.Addr(), 3, uintptr(unsafe.Pointer(src)), uintptr(unsafe.Pointer(dst)), uintptr(size)) + n = uint32(r0) + if n == 0 { + err = errnoErr(e1) + } + return +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c4d65458..9671af4a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -193,6 +193,7 @@ golang.org/x/sys/cpu golang.org/x/sys/plan9 golang.org/x/sys/unix golang.org/x/sys/windows +golang.org/x/sys/windows/registry # golang.org/x/term v0.32.0 ## explicit; go 1.23.0 golang.org/x/term From 1afb512b30edb9765fc4022532c08974e3d54e94 Mon Sep 17 00:00:00 2001 From: Paul Querna Date: Wed, 1 Jul 2026 19:30:31 +0000 Subject: [PATCH 2/4] Read macOS managed config via native CoreFoundation under cgo Split the macOS managed-config reader by build tag: - managedconfig_darwin_cgo.go (darwin && cgo): read the "ai.c1" managed-preferences store natively via CFPreferencesCopyAppValue, the managed-prefs-aware CoreFoundation API, instead of shelling out. Memory handling follows the crypto/x509/internal/macos idioms: every Create/Copy ref is released, the returned value is type-checked as a CFString before extraction, and the string is read without assuming a fixed buffer (CFStringGetCStringPtr fast path, CFStringGetCString fallback sized by CFStringGetMaximumSizeForEncoding). Absent key, non-string value, or any failure yields an empty result; never panics or errors. - managedconfig_darwin_defaults.go (darwin && !cgo): the existing `defaults read` path, kept as the CGO-off fallback so pure-Go cross-compilation (e.g. darwin/arm64 from Linux, as the release build does) still works. Retains the stubbable defaultsRead var and reuses parseDefaultsValue. Exactly one file compiles per (darwin, cgo?) combination; both expose the same readManagedConfig entrypoint and preserve the existing contract (single-key read, unknown keys ignored, absent/malformed -> empty, TenantDomain validated as a full DNS host downstream). Co-authored-by: c1-squire-dev[bot] --- pkg/managedconfig/managedconfig_darwin_cgo.go | 104 ++++++++++++++++++ ...in.go => managedconfig_darwin_defaults.go} | 7 +- 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 pkg/managedconfig/managedconfig_darwin_cgo.go rename pkg/managedconfig/{managedconfig_darwin.go => managedconfig_darwin_defaults.go} (72%) diff --git a/pkg/managedconfig/managedconfig_darwin_cgo.go b/pkg/managedconfig/managedconfig_darwin_cgo.go new file mode 100644 index 00000000..fce92967 --- /dev/null +++ b/pkg/managedconfig/managedconfig_darwin_cgo.go @@ -0,0 +1,104 @@ +//go:build darwin && cgo + +package managedconfig + +/* +#cgo LDFLAGS: -framework CoreFoundation +#include +#include +*/ +import "C" + +import "unsafe" + +// readManagedConfig reads the managed device configuration from the macOS +// managed-preferences store using CoreFoundation directly. It calls +// CFPreferencesCopyAppValue, which resolves the managed-preferences layer +// (administrator-pushed policy) for the "ai.c1" application domain, exactly as +// the `defaults read` fallback does — but without shelling out. +// +// A missing key, a value that is not a string, or any lookup failure yields a +// nil map. This function never returns an error and never panics. +func readManagedConfig() map[string]string { + v := copyManagedString(Namespace, KeyTenantDomain) + if v == "" { + return nil + } + return map[string]string{KeyTenantDomain: v} +} + +// copyManagedString reads a single string value from the managed-preferences +// layer for the given application domain, returning "" when the key is absent, +// is not a string, or cannot be read. +// +// Memory ownership follows the CoreFoundation "Create/Copy" rule mirrored from +// crypto/x509/internal/macos: every ref obtained from a Create*/Copy* call is +// owned (+1 retain) and must be released; refs obtained from Get* calls are not +// owned and must not be released. +func copyManagedString(appID, key string) string { + // C.CString allocates C memory that must be freed explicitly. + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + cApp := C.CString(appID) + defer C.free(unsafe.Pointer(cApp)) + + // CFStringCreateWithCString returns an owned (+1) CFString, or NULL if the + // bytes are not valid for the encoding. Release both when done. + keyRef := C.CFStringCreateWithCString(C.kCFAllocatorDefault, cKey, C.kCFStringEncodingUTF8) + if keyRef == nil { + return "" + } + defer C.CFRelease(C.CFTypeRef(keyRef)) + + appRef := C.CFStringCreateWithCString(C.kCFAllocatorDefault, cApp, C.kCFStringEncodingUTF8) + if appRef == nil { + return "" + } + defer C.CFRelease(C.CFTypeRef(appRef)) + + // CFPreferencesCopyAppValue returns an owned (+1) value, or NULL when the + // key is absent. Release it whether or not it is the type we want. + val := C.CFPreferencesCopyAppValue(keyRef, appRef) + if val == nil { + return "" + } + defer C.CFRelease(val) + + // The value may be any property-list type. Confirm it is actually a + // CFString before extracting; a non-string value yields "" rather than a + // crash. + if C.CFGetTypeID(val) != C.CFStringGetTypeID() { + return "" + } + + return cfStringToGoString(C.CFStringRef(val)) +} + +// cfStringToGoString extracts a Go string from a CFString without assuming a +// fixed buffer size, mirroring the robust extraction in +// crypto/x509/internal/macos: try the zero-copy fast path first, then fall back +// to CFStringGetCString with a buffer sized by CFStringGetMaximumSizeForEncoding. +// The caller retains ownership of s; this function releases nothing it does not +// create. +func cfStringToGoString(s C.CFStringRef) string { + // Fast path: CFStringGetCStringPtr may return a pointer to the CFString's + // internal UTF-8 storage. It is owned by the CFString (not by us), so it is + // copied by C.GoString and never released. It returns NULL when no such + // direct representation exists, in which case we fall back below. + if p := C.CFStringGetCStringPtr(s, C.kCFStringEncodingUTF8); p != nil { + return C.GoString(p) + } + + // Fallback: size a buffer large enough for the worst-case UTF-8 encoding of + // the string's length, plus one for the NUL terminator, then copy into it. + length := C.CFStringGetLength(s) + maxSize := C.CFStringGetMaximumSizeForEncoding(length, C.kCFStringEncodingUTF8) + 1 + buf := (*C.char)(C.malloc(C.size_t(maxSize))) + defer C.free(unsafe.Pointer(buf)) + + // CFStringGetCString returns a Boolean (0 on failure). + if C.CFStringGetCString(s, buf, maxSize, C.kCFStringEncodingUTF8) == 0 { + return "" + } + return C.GoString(buf) +} diff --git a/pkg/managedconfig/managedconfig_darwin.go b/pkg/managedconfig/managedconfig_darwin_defaults.go similarity index 72% rename from pkg/managedconfig/managedconfig_darwin.go rename to pkg/managedconfig/managedconfig_darwin_defaults.go index b77cc896..73dcb6a2 100644 --- a/pkg/managedconfig/managedconfig_darwin.go +++ b/pkg/managedconfig/managedconfig_darwin_defaults.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build darwin && !cgo package managedconfig @@ -8,6 +8,11 @@ import "os/exec" // tool. Reading via the managed-preferences layer (rather than the on-disk // plist) ensures only administrator-pushed policy is honored. It is a variable // so tests can stub the lookup. +// +// This CGO-off fallback exists so cone still cross-compiles for darwin from a +// pure-Go toolchain (for example darwin/arm64 built on Linux with +// CGO_ENABLED=0). The darwin && cgo build uses the native CoreFoundation reader +// in managedconfig_darwin_cgo.go instead. var defaultsRead = func(domain, key string) ([]byte, error) { return exec.Command("defaults", "read", domain, key).Output() } From 86d3d2ebc5c21e2fd30d6573469dcc610d39cf17 Mon Sep 17 00:00:00 2001 From: Paul Querna Date: Wed, 1 Jul 2026 20:51:38 +0000 Subject: [PATCH 3/4] Check registry key Close error in Windows managed-config reader golangci-lint (errcheck) flags the unchecked k.Close() return on the GOOS=windows build of the managed-config reader. Defer an explicit discard so the intent is clear and the Windows build lints clean. Co-authored-by: c1-squire-dev[bot] --- pkg/managedconfig/managedconfig_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/managedconfig/managedconfig_windows.go b/pkg/managedconfig/managedconfig_windows.go index aeadfba6..548702bf 100644 --- a/pkg/managedconfig/managedconfig_windows.go +++ b/pkg/managedconfig/managedconfig_windows.go @@ -14,7 +14,7 @@ func readManagedConfig() map[string]string { if err != nil { return nil } - defer k.Close() + defer func() { _ = k.Close() }() val, _, err := k.GetStringValue(KeyTenantDomain) if err != nil { From 0b8da2476a84bb76cc097b6d6d297f7e536f8e57 Mon Sep 17 00:00:00 2001 From: Paul Querna Date: Wed, 1 Jul 2026 20:51:38 +0000 Subject: [PATCH 4/4] Fix staticcheck SA5011 nil-deref-after-check in secret_test Each nil guard used t.Fatal without a terminal statement, so staticcheck (SA5011) sees control fall through to the pointer dereference on the nil path. Add an explicit return after t.Fatal so the deref is unreachable when the pointer is nil. Semantics unchanged; unblocks CI go-lint. Co-authored-by: c1-squire-dev[bot] --- cmd/cone/secret_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/cone/secret_test.go b/cmd/cone/secret_test.go index 78568552..035c2ced 100644 --- a/cmd/cone/secret_test.go +++ b/cmd/cone/secret_test.go @@ -127,6 +127,7 @@ func TestCreateInputFormat(t *testing.T) { got := createInputFormat(tt.name) if got == nil { t.Fatal("createInputFormat() returned nil") + return } if *got != tt.want { t.Errorf("createInputFormat(%q) = %q, want %q", tt.name, *got, tt.want) @@ -208,6 +209,7 @@ func TestCreateSecretInternalText(t *testing.T) { r := f.internalReq if r == nil { t.Fatal("internal request was not built") + return } if r.SecretType == nil || *r.SecretType != shared.PaperSecretServiceCreateInternalRequestSecretTypeSecretTypeText { t.Errorf("SecretType = %v, want Text", r.SecretType) @@ -246,6 +248,7 @@ func TestCreateSecretInternalFile(t *testing.T) { r := f.internalReq if r == nil { t.Fatal("internal request was not built") + return } if r.SecretType == nil || *r.SecretType != shared.PaperSecretServiceCreateInternalRequestSecretTypeSecretTypeFile { t.Errorf("SecretType = %v, want File", r.SecretType) @@ -283,6 +286,7 @@ func TestCreateSecretExternalText(t *testing.T) { r := f.externalReq if r == nil { t.Fatal("external request was not built") + return } if r.SecretType == nil || *r.SecretType != shared.PaperSecretServiceCreateExternalRequestSecretTypeSecretTypeText { t.Errorf("SecretType = %v, want Text", r.SecretType) @@ -311,6 +315,7 @@ func TestCreateSecretExternalFile(t *testing.T) { r := f.externalReq if r == nil { t.Fatal("external request was not built") + return } if r.SecretType == nil || *r.SecretType != shared.PaperSecretServiceCreateExternalRequestSecretTypeSecretTypeFile { t.Errorf("SecretType = %v, want File", r.SecretType) @@ -347,6 +352,7 @@ func TestCreateExternalInputFormat(t *testing.T) { got := createExternalInputFormat(tt.name) if got == nil { t.Fatal("createExternalInputFormat() returned nil") + return } if *got != tt.want { t.Errorf("createExternalInputFormat(%q) = %q, want %q", tt.name, *got, tt.want) @@ -405,6 +411,7 @@ func TestSetContentInputFormat(t *testing.T) { got := setContentInputFormat(tt.name) if got == nil { t.Fatal("setContentInputFormat() returned nil") + return } if *got != tt.want { t.Errorf("setContentInputFormat(%q) = %q, want %q", tt.name, *got, tt.want)