Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions cmd/cone/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tenant-name or tenant-url>",
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 {
Expand Down
7 changes: 7 additions & 0 deletions cmd/cone/secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
113 changes: 113 additions & 0 deletions pkg/managedconfig/managedconfig.go
Original file line number Diff line number Diff line change
@@ -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 <domain> <key>`
// 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}
}
104 changes: 104 additions & 0 deletions pkg/managedconfig/managedconfig_darwin_cgo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//go:build darwin && cgo

package managedconfig

/*
#cgo LDFLAGS: -framework CoreFoundation
#include <stdlib.h>
#include <CoreFoundation/CoreFoundation.h>
*/
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)
}
28 changes: 28 additions & 0 deletions pkg/managedconfig/managedconfig_darwin_defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//go:build darwin && !cgo

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.
//
// 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()
}

// 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)
}
19 changes: 19 additions & 0 deletions pkg/managedconfig/managedconfig_linux.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading