Skip to content

Add managed device configuration and default device-flow login#145

Open
c1-squire-dev[bot] wants to merge 4 commits into
mainfrom
paul.querna/managed-config-login
Open

Add managed device configuration and default device-flow login#145
c1-squire-dev[bot] wants to merge 4 commits into
mainfrom
paul.querna/managed-config-login

Conversation

@c1-squire-dev

@c1-squire-dev c1-squire-dev Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a way for cone to auto-discover the tenant it belongs to from managed device configuration — administrator-defined policy delivered to a device through an MDM — so users don't have to configure the tenant by hand. When such policy is present, a bare cone login (no argument) discovers the tenant automatically and goes straight into the existing OAuth 2.0 Device Authorization Grant flow.

What's included

1. New package: pkg/managedconfig

Reads the company-level ai.c1 managed store, OS-selected at build time (read-only):

OS Store
Linux /etc/c1/managed.toml (TOML)
macOS the ai.c1 managed-preferences domain (managed layer, not the on-disk plist)
Windows HKLM\SOFTWARE\Policies\ConductorOne\C1, value TenantDomain (REG_SZ)

On macOS the reader is split by build tag so it works both natively and under pure-Go cross-compilation:

  • darwin && cgo → native CoreFoundation: CFPreferencesCopyAppValue("TenantDomain", "ai.c1"), the managed-preferences-aware API, with no subprocess. 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 via CFStringGetMaximumSizeForEncoding). An absent key, a non-string value, or any failure yields an empty result — it never errors or panics.
  • darwin && !cgo → the existing defaults read path, kept as the CGO-off fallback so pure-Go cross-compilation (e.g. darwin/arm64 from Linux, which is how the release binaries are built) still works.

Exactly one of the two compiles per build; both expose the same reader and preserve identical semantics.

Semantics:

  • The store is read as a key/value map; unknown keys are ignored.
  • 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 v1 key TenantDomain is the full DNS host of the tenant's control plane (e.g. acme.conductor.one, acme.eu.c1.ai); a bare slug is rejected. Config.ControlPlaneURL() derives https://{TenantDomain}.

2. cone login reads managed config first

resolveLoginTenant consults managed configuration before the positional argument:

  • Managed config present → tenant discovered automatically; cone login needs no argument and proceeds directly into the device code flow.
  • No managed config → behavior is unchanged; the tenant must be supplied as an argument.

The device authorization flow itself is cone's existing conductoroneapi.LoginFlow with the existing public client ID — no new client ID and no client secret is introduced.

Testing

  • go build ./..., go vet, and golangci-lint run are clean.
  • go test ./pkg/managedconfig/... passes. The Linux TOML path is unit-tested end to end (valid parse, unknown-key-ignored, absent file, malformed file, bare-slug rejection); OS-independent parsing/validation helpers are covered directly.
  • Cross-compiles verified for windows/amd64 and, with CGO_ENABLED=0, for darwin/amd64 and darwin/arm64 (the CGO-off defaults fallback — this is the configuration the release binaries ship).
  • The darwin && cgo native reader is not built by CI (CI runs on Linux and the release build cross-compiles darwin with CGO_ENABLED=0), so it needs a macOS build with CGO_ENABLED=1 to validate the CoreFoundation linkage. It was written to the crypto/x509/internal/macos CF idioms and gofmted, but not compiled here — a reviewer on a Mac (or a macOS+cgo CI job) should confirm it builds.

CI lint

Also fixes a pre-existing staticcheck SA5011 (nil-deref-after-check) in cmd/cone/secret_test.go — each if x == nil { t.Fatal(...) } guard now has an explicit return, so CI go-lint is green. Additionally handles the previously-unchecked k.Close() error in the Windows reader (errcheck on the GOOS=windows build).

pquerna and others added 4 commits July 1, 2026 19:14
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] <c1-squire-dev[bot]@users.noreply.github.com>
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] <c1-squire-dev[bot]@users.noreply.github.com>
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] <c1-squire-dev[bot]@users.noreply.github.com>
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] <c1-squire-dev[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant