config

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package config provides centralized configuration management for the application. It has no external dependency on other internal packages to avoid import cycles.

Per the Open CLI Collective Secret-Handling Standard §1.2 / §2.3, no access secret is ever written here. The per-user OAuth token lives only in the OS keyring (via cli-common's credstore). The OAuth client JSON is deployment material (§1.2) and lives in a plain file referenced by oauth_client_path. This file owns config.yml: the authoritative credential_ref (§1.3), the OAuth client JSON path, the optional §1.4 file-backend opt-in, and the pre-existing non-secret tuning (cache TTL, granted scopes).

Index

Constants

View Source
const (
	// CredentialsFile is the legacy OAuth client JSON filename (deployment
	// material). Superseded by OAuthClientFile; retained so the one-time
	// migration can find and relocate it.
	CredentialsFile = "credentials.json"
	// OAuthClientFile is the post-migration OAuth client JSON filename
	// (deployment material per §1.2 — not a secret, not in the keyring).
	OAuthClientFile = "oauth_client.json"
	// TokenFile is the legacy OAuth token fallback filename. Superseded by
	// the keyring; retained so the one-time migration can find it.
	TokenFile = "token.json"
	// ConfigFile is the legacy JSON config filename, read once for
	// transparent upgrade to ConfigFileYAML.
	ConfigFile = "config.json"
	// ConfigFileYAML is the authoritative config filename.
	ConfigFileYAML = "config.yml"
)
View Source
const (
	// DirPerm is the permission for config directories (owner rwx only).
	DirPerm = 0700
	// TokenPerm is the permission for the config file (owner rw only). The
	// config holds no secret, but there is no reason for it to be world
	// readable.
	TokenPerm = 0600
	// OutputDirPerm is the permission for output directories.
	OutputDirPerm = 0755
	// OutputFilePerm is the permission for output files, and for the OAuth
	// client JSON (deployment material — non-secret, org-internal).
	OutputFilePerm = 0644
)

File and directory permission constants for consistent security settings.

Variables

View Source
var DefaultCredentialRef string

DefaultCredentialRef is the default <service>/<profile> credential ref for the registered CLI. Populated by Register; empty until then.

View Source
var DirName string

DirName is the config/cache directory name and keyring service segment for the registered CLI. Populated by Register; empty until then.

View Source
var ErrRelocationConflict = errors.New("config: shared old/new config diverge")

ErrRelocationConflict is returned by LoadConfig (and surfaced through LoadConfigForRuntime) when both the old hand-rolled config dir and the new statedir-resolved config dir contain a config.yml with materially different user settings. Mutation-free: nothing is copied, nothing is overwritten. The user reconciles by running `gro init` (which fails the same way at its pre-write gate) or by manually deleting one side.

Functions

func ApplyConfigRelocation

func ApplyConfigRelocation(r SharedRelocation) error

ApplyConfigRelocation copies every file in the old config dir to the new dir EXCEPT token.json (the only access secret; handled by keychain.migrate via §1.8 conflict semantics — see MON-5371 plan). Idempotent: if the new dir already has a same-named file, that one is left untouched. Files are written via temp+rename at 0600 under a 0700 dir. The old dir is not modified — leave-old gives the user a recovery point and matches the MON-5370 family pattern.

func CacheDirPath

func CacheDirPath() (string, error)

CacheDirPath resolves the OS-designated cache directory WITHOUT creating it (used by `config clear --all --dry-run` and tests). os.UserCacheDir gives the canonical per-OS root: Linux $XDG_CACHE_HOME or ~/.cache, macOS ~/Library/Caches, Windows %LocalAppData%. We append only DirName — no platform-specific suffix — to keep all three consistent.

func DefaultOAuthClientPath

func DefaultOAuthClientPath() (string, error)

DefaultOAuthClientPath is the expanded absolute default for OAuthClientPath: <configdir>/oauth_client.json.

func ExpandPath

func ExpandPath(p string) string

ExpandPath resolves a leading ~ and makes the path absolute. Stored config values are always expanded; ~ is for display only (ShortenPath).

func GetCacheDir

func GetCacheDir() (string, error)

GetCacheDir returns the cache directory, creating it if needed.

func GetConfigDir

func GetConfigDir() (string, error)

GetConfigDir returns the configuration directory, creating it if needed.

func GetConfigDirNoCreate

func GetConfigDirNoCreate() (string, error)

GetConfigDirNoCreate resolves the configuration directory WITHOUT creating it — for side-effect-free callers such as `config clear --all` (incl. `--dry-run`). Symmetric with GetConfigPathNoCreate.

func GetConfigPath

func GetConfigPath() (string, error)

GetConfigPath returns the authoritative config file path (config.yml).

func GetConfigPathNoCreate

func GetConfigPathNoCreate() (string, error)

GetConfigPathNoCreate is GetConfigPath WITHOUT creating the config dir — for side-effect-free paths such as `config clear --dry-run`.

func GetCredentialsPath

func GetCredentialsPath() (string, error)

GetCredentialsPath returns the path to the LEGACY credentials.json (deployment material). Used by the one-time migration to relocate it to OAuthClientPath; not the runtime client-JSON source post-migration.

func GetTokenPath

func GetTokenPath() (string, error)

GetTokenPath returns the path to the LEGACY token.json fallback. Used only by the one-time migration into the keyring.

func LegacyCacheDir

func LegacyCacheDir() (string, error)

LegacyCacheDir resolves the pre-B2b cache directory (a "cache" subdir of the config dir) WITHOUT creating anything — for the one-time relocation only.

func LegacyConfigJSONPath

func LegacyConfigJSONPath() (string, error)

LegacyConfigJSONPath returns the pre-migration config.json path.

func OldHandRolledConfigDir

func OldHandRolledConfigDir() (string, error)

OldHandRolledConfigDir is the pre-MON-5371 hand-rolled config directory (~/.config/google-readonly on macOS/Windows; identical to the canonical dir on Linux). Exposed so cleanup commands can scrub legacy config files at this location — symmetric with LegacyCacheDir on the cache side. Non-creating. Returns ("", error) if HOME is unresolvable.

func OldHandRolledLegacyCacheDir

func OldHandRolledLegacyCacheDir() (string, error)

OldHandRolledLegacyCacheDir is the pre-B2b cache subdir under the pre- MON-5371 hand-rolled config dir (a real artifact on macOS/Windows installs that pre-date both B2b and MON-5371). cli-common does NOT resolve here, so internal/cache's migrator probes both this path and the in-resolver LegacyCacheDir, byte-carrying whichever is present. On Linux this resolves identical to LegacyCacheDir and dedupes inside the migrator.

func OldHandRolledTokenPath

func OldHandRolledTokenPath() (string, error)

OldHandRolledTokenPath is the pre-MON-5371 legacy token.json location. Exported so keychain.migrate's token-source enumeration can probe it with full §1.8 conflict semantics (per the MON-5371 plan: token.json is excluded from ApplyConfigRelocation and handled exclusively through the existing migrator).

func ProductName

func ProductName() string

ProductName returns the registered user-facing product/command name.

func Register

func Register(id Identity)

Register stores the process-wide CLI identity. Call it exactly once from main, before any other config/keychain/auth call. It panics if DirName or DefaultRef is empty, because every downstream path (keyring service, config dir, env-var names) derives from them and a silent empty value would resolve to the wrong — possibly shared — location.

func RegisterForTest

func RegisterForTest()

RegisterForTest registers a canonical identity for the shared library's own tests (and for any consumer test helper that needs a valid identity without caring about the specific CLI). It mirrors the historical google-readonly values so identity-specific assertions ported from that CLI keep passing. It is exported (not a _test.go helper) so sibling test packages such as credtest can call it without an import cycle; production CLIs never call it — they call Register with their own Identity from main.

func SaveConfig

func SaveConfig(cfg *Config) error

SaveConfig writes config.yml at 0600 under a 0700 directory using an atomic temp-file-in-same-dir → chmod 0600 → rename (§3 standard). The unique temp name from os.CreateTemp means a same-process concurrent save and a crash-leftover from a prior run can never collide; a hard-crash orphan tmp is harmless (never read as config). OAuthClientPath is persisted expanded + absolute so os.ReadFile never sees a literal ~.

func ScopeDescriptions

func ScopeDescriptions() map[string]string

ScopeDescriptions returns the registered scope-description map (may be nil).

func Scopes

func Scopes() []string

Scopes returns a copy of the registered OAuth scope set.

func ShortenPath

func ShortenPath(path string) string

ShortenPath replaces the home directory prefix with ~ for display, so errors and `config show` do not expose usernames.

func SiblingOAuthClientPath

func SiblingOAuthClientPath() (path, siblingDirName string, ok bool)

SiblingOAuthClientPath returns the path to an existing OAuth client JSON belonging to a registered sibling CLI (deployment material, not a secret), so init can reuse it instead of asking the user to paste it again. It returns the first sibling whose oauth_client.json exists, along with that sibling's DirName. ok is false when no sibling has one.

Types

type Config

type Config struct {
	// CredentialRef is the authoritative <service>/<profile> keyring ref
	// (§1.3). Resolved via credstore.ParseRef; never hard-coded.
	CredentialRef string `yaml:"credential_ref" json:"credential_ref,omitempty"`
	// OAuthClientPath is the absolute path to the OAuth client JSON
	// (deployment material). Stored expanded + absolute; `~` is display-only
	// via ShortenPath. An org may override the default location here.
	OAuthClientPath string `yaml:"oauth_client_path" json:"oauth_client_path,omitempty"`
	// GrantedScopes is preserved: detects when a token's scopes drift from
	// what init granted. Not a secret.
	GrantedScopes []string `yaml:"granted_scopes,omitempty" json:"granted_scopes,omitempty"`
	// Keyring carries the optional §1.4 explicit file-backend opt-in.
	Keyring KeyringConfig `yaml:"keyring,omitempty" json:"-"`
}

Config is google-readonly's config.yml. Everything here is safe for an org to ship via MDM (§1.2); none of it is an access secret. JSON tags are retained so a legacy config.json is read transparently for one upgrade.

The pre-MON-5371 `cache_ttl_hours` field is gone — cache TTL is now hard-coded per resource per cli-common/docs/working-with-state.md §4.4. An older config.yml that still contains `cache_ttl_hours: N` continues to load cleanly (yaml.v3 silently ignores unknown fields); the value is just inert post-port.

func LoadConfig

func LoadConfig() (*Config, error)

LoadConfig loads config.yml. The strict variant — used by `gro init`'s relocation gate and by tests. Returns ErrRelocationConflict (with a wrapped detail message) when both the old hand-rolled and new statedir-resolved dirs contain materially-different config.yml files; on conflict, the canonical new-dir config is still returned alongside the error so callers can choose to soft-degrade. Runtime call sites should use LoadConfigForRuntime instead — see relocate.go.

If new/config.yml is absent but old-only is present (the MON-5371 macOS/Windows pre-init steady state), the old file is transparently read. If neither YAML is present, a legacy config.json is read once at the new dir (post-init); if that is also absent, defaults are returned.

Defaults are always applied to the returned *Config.

func LoadConfigForRuntime

func LoadConfigForRuntime() (*Config, error)

LoadConfigForRuntime is the soft-conflict variant of LoadConfig for non-init callers. On ErrRelocationConflict it prints a one-shot stderr warning, then returns the canonical (new-dir) config so the command can keep working — BUT only when a canonical config was actually read. If LoadConfig couldn't populate cfg (e.g. malformed YAML/JSON on the canonical side), the runtime must hard-fail instead of warning-and-defaulting, otherwise it would silently swap CredentialRef etc. back to defaults and mask the corrupt file. Init uses strict LoadConfig (fail-loud) at its relocation gate.

type Identity

type Identity struct {
	// DirName is the config/cache dir name, keyring service segment, and
	// env-var prefix source (e.g. "google-readonly").
	DirName string
	// DefaultRef is the default <service>/<profile> credential ref applied
	// when config.yml is absent or omits credential_ref (e.g.
	// "google-readonly/default"). Callers still resolve it via
	// credstore.ParseRef — the structure is never assumed (§1.3).
	DefaultRef string
	// ProductName is the user-facing command/product name spliced into
	// messages (e.g. "gro" -> "run 'gro init'").
	ProductName string
	// Scopes is the OAuth scope set requested at init and validated for drift.
	Scopes []string
	// ScopeDescriptions maps scope URLs to human-friendly descriptions used by
	// the init wizard and the scope-drift re-auth prompt.
	ScopeDescriptions map[string]string
	// SiblingDirNames lists the DirNames of sibling CLIs whose OAuth client
	// JSON (deployment material, not a secret) this CLI may reuse during init,
	// so a user who has already set up a sibling need not paste it again. E.g.
	// grw lists "google-readonly" so `grw init` can adopt gro's OAuth client.
	SiblingDirNames []string
}

Identity carries the per-CLI values that were previously compile-time constants in google-readonly (DirName, DefaultCredentialRef) plus the OAuth scope set and user-facing product name. It is the single seam that lets one shared library back multiple Google CLIs (e.g. gro, grw): each CLI process registers its identity exactly once at startup — from main, before any other config/keychain/auth call — so the library resolves the correct config/cache dir, keyring service, env-var prefixes, requested scopes, and message wording for that CLI.

DirName drives everything downstream: the native per-OS config dir and cache dir (via cli-common/statedir), the keyring service segment, and the derived env-var prefixes (<SERVICE>_KEYRING_BACKEND, <SERVICE>_KEYRING_PASSPHRASE, <SERVICE>_CREDENTIAL_REF). DefaultRef is the fallback <service>/<profile> credential ref when config.yml omits credential_ref.

type KeyringConfig

type KeyringConfig struct {
	Backend string `yaml:"backend,omitempty" json:"-"`
}

KeyringConfig is the §1.4 backend selector. Backend == "file" forces the encrypted-file backend; empty means OS default selection (fail-closed on Linux when no Secret Service is available).

type SharedRelocation

type SharedRelocation struct {
	Kind       relocKind
	OldPath    string // old hand-rolled config dir; "" on Linux short-circuit
	NewPath    string // statedir-resolved config dir (always set)
	CopyNeeded bool   // relocOldOnly only
}

SharedRelocation is the result of DetectConfigRelocation. Paths are filled even on relocNone so callers can log/diagnose; CopyNeeded is true iff a gated ApplyConfigRelocation would actually do work.

func DetectConfigRelocation

func DetectConfigRelocation() (SharedRelocation, error)

DetectConfigRelocation classifies the old/new pair without touching disk beyond stats and reads. Never copies, never writes. On Linux (old==new) it short-circuits to relocNone. On macOS/Windows it returns one of the four kinds and the named paths.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL