session

package
v0.38.0 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: Apache-2.0 Imports: 33 Imported by: 0

Documentation

Overview

Package session is the dtctl session layer: everything a Dynatrace CLI tool needs to go from "the user's machine" to "an authenticated client for the right tenant".

It owns the shared state contract (docs/dev/CONFIG_CONTRACT.md):

  • the config-file model — contexts, tokens, preferences, safety levels — with discovery, env expansion, schema versioning, and round-trip preservation of unknown fields (golden fixtures in testdata/contract)
  • credential resolution — OS keyring (service "dtctl"), file-based OAuth store fallback, inline config tokens

dtctl and dtctl-* plugins all consume this package, which is what makes "drop into any of them wherever dtctl points" work. Configuration management (creating contexts, login flows) is dtctl's job alone; other consumers treat the config file as read-only.

The alias, hook, and spill fields carried by Config are CLI-owned schema data: this package round-trips and exposes them, but never executes them — alias expansion and hook execution live in dtctl's cmd layer.

Index

Constants

View Source
const (
	// KeyringService is the service name used for keyring storage
	KeyringService = "dtctl"

	// EnvDisableKeyring can be set to disable keyring integration
	EnvDisableKeyring = "DTCTL_DISABLE_KEYRING"

	// EnvTokenStorage controls the OAuth token storage backend.
	// Set to "file" to use file-based storage instead of the OS keyring.
	// This is useful for headless Linux, WSL, CI/CD, and container environments
	// where a system keyring is not available.
	//
	// Valid values: "keyring" (default), "file"
	EnvTokenStorage = "DTCTL_TOKEN_STORAGE"

	// ErrMsgCollectionUnlock is the error substring returned by the Secret Service
	// backend when a persistent keyring collection does not exist or cannot be
	// unlocked. Centralised here so callers match on a single constant instead
	// of a fragile raw string.
	ErrMsgCollectionUnlock = "failed to unlock correct collection"
)
View Source
const (
	// OAuthTokenPrefix is prepended to OAuth token names in keyring
	OAuthTokenPrefix = "oauth:"

	// TokenRefreshBuffer is how long before expiry we refresh tokens
	TokenRefreshBuffer = 5 * time.Minute
)
View Source
const CurrentAPIVersion = "v1"

CurrentAPIVersion is the config schema version this build reads and writes. The schema evolves additively within a version: unknown fields are ignored on load and preserved on save (see SaveTo), so the version only changes on a breaking redefinition of existing fields. See docs/dev/CONFIG_CONTRACT.md.

View Source
const EnvConfig = "DTCTL_CONFIG"

EnvConfig names an explicit config file, equivalent to passing --config. When set, dtctl loads exactly that file and treats it as trusted (its aliases and apply hooks are honored), skipping auto-discovery entirely. This is the supported way to run in a prepared, trusted workspace — e.g. kb-run creates a clean directory with a .dtctl.yaml and exports this variable, so an agent invoking `dtctl apply` runs the intended hooks without a --config flag. Because it names a specific operator-chosen file (not a directory to search), it does not reopen the untrusted-working-directory vector that IsLocal() guards against: a stray .dtctl.yaml elsewhere on disk can never be picked up.

View Source
const LocalConfigName = ".dtctl.yaml"

LocalConfigName is the name of the per-project config file

View Source
const ProfileEnvVar = "DTCTL_PROFILE"

ProfileEnvVar is the environment variable that selects the active command profile, taking precedence over any context-bound profile.

View Source
const ProfileFull = "full"

ProfileFull is the reserved name for the unrestricted command tree. Selecting it (via env or context) is equivalent to selecting no profile at all.

Variables

View Source
var ErrOAuthSessionRevoked = errors.New("OAuth session revoked")

ErrOAuthSessionRevoked indicates the cached OAuth refresh token has been invalidated server-side (HTTP 400 invalid_grant). Callers should evict the cache and fall back to a non-OAuth credential where available.

Functions

func BuiltinProfileNames

func BuiltinProfileNames() []string

BuiltinProfileNames returns the sorted names of the built-in presets (excluding the special "full" profile). Used for docs/validation surfaces.

func CacheDir

func CacheDir() string

CacheDir returns the cache directory path following XDG Base Directory spec

func CheckKeyring

func CheckKeyring() error

CheckKeyring probes the OS keyring and returns nil if it is usable, or a descriptive error explaining why it is not.

func ConfigDir

func ConfigDir() string

ConfigDir returns the config directory path following XDG Base Directory spec

func DataDir

func DataDir() string

DataDir returns the data directory path following XDG Base Directory spec

func DecodeRefreshTokenExpiry

func DecodeRefreshTokenExpiry(refreshToken string) (time.Time, bool)

DecodeRefreshTokenExpiry returns the exp claim from a JWT refresh token. Returns zero time and false if the token is not a decodable JWT with an exp claim.

func DefaultConfigPath

func DefaultConfigPath() string

DefaultConfigPath returns the default config file path following XDG Base Directory spec Returns: XDG_CONFIG_HOME/dtctl/config (typically ~/.config/dtctl/config)

func EnsureKeyringCollection

func EnsureKeyringCollection(ctx context.Context) error

EnsureKeyringCollection checks whether a usable Secret Service collection exists and, if not, creates a persistent "login" collection. On Linux/WSL gnome-keyring may start with only a transient "session" collection; this function creates the permanent one, which may trigger an OS password prompt.

The provided context allows the caller to cancel the operation (e.g. via Ctrl+C); without cancellation the function polls for up to 2 minutes waiting for the user to complete the password prompt.

func ExtractUserIDFromToken

func ExtractUserIDFromToken(token string) (string, error)

ExtractUserIDFromToken extracts the user ID (sub claim) from a JWT token.

func FindLocalConfig

func FindLocalConfig() string

FindLocalConfig searches for a .dtctl.yaml file starting from the current directory and walking up to the root. Returns empty string if not found.

func GetTokenForContext

func GetTokenForContext(cfg *Config, environmentURL, tokenRef string) (string, error)

GetTokenForContext retrieves a token from config with OAuth token refresh support, detecting the OAuth configuration from the supplied environment URL. Use this when the token may belong to a context other than the current one (e.g. `dtctl ctx token <name>`), since the OAuth environment determines both the refresh endpoint and the storage key.

func GetTokenWithFallback

func GetTokenWithFallback(cfg *Config, tokenRef string) (string, error)

GetTokenWithFallback tries to get a token from keyring first, then falls back to config

func GetTokenWithOAuthSupport

func GetTokenWithOAuthSupport(cfg *Config, tokenRef string) (string, error)

GetTokenWithOAuthSupport retrieves a token from config with OAuth token refresh support, using the current context's environment to detect the OAuth configuration.

func IsFileTokenStorage

func IsFileTokenStorage() bool

IsFileTokenStorage reports whether the user has explicitly opted into file-based OAuth token storage via DTCTL_TOKEN_STORAGE=file.

func IsKeyringAvailable

func IsKeyringAvailable() bool

IsKeyringAvailable checks if keyring storage is available on this system

func IsOAuthStorageAvailable

func IsOAuthStorageAvailable() bool

IsOAuthStorageAvailable reports whether OAuth tokens can be stored and retrieved — either via the OS keyring or file-based storage.

func IsOAuthToken

func IsOAuthToken(tokenName string) bool

IsOAuthToken checks if a token name refers to an OAuth token

func IsPlatformToken

func IsPlatformToken(token string) bool

platformTokenPrefix identifies Dynatrace platform tokens — opaque bearer tokens (not JWTs). The /platform/metadata/v1/user endpoint requires the 'app-engine:apps:run' scope; platform tokens that lack it get a 403, and the token itself cannot be JWT-decoded as a user-ID fallback. IsPlatformToken reports whether token is a Dynatrace platform token.

func IsTokenExpired

func IsTokenExpired(tokens *TokenSet) bool

IsTokenExpired checks if a token is expired

func KeyringBackend

func KeyringBackend() string

KeyringBackend returns a string describing the keyring backend in use

func MigrateTokensToKeyring

func MigrateTokensToKeyring(cfg *Config) (int, error)

MigrateTokensToKeyring migrates tokens from config file to keyring Returns the number of tokens migrated and any error

func OAuthStorageBackend

func OAuthStorageBackend() string

OAuthStorageBackend returns a human-readable label describing where OAuth tokens are (or will be) stored.

func RefreshedTokenForContext

func RefreshedTokenForContext(cfg *Config, environmentURL, tokenRef, rejected string) (string, error)

RefreshedTokenForContext re-resolves the context's bearer token after a request was rejected with HTTP 401. GetTokenForContext already refreshes tokens that are near expiry locally; when the cached token still looks valid but the server rejected it anyway (clock skew, stale ExpiresAt in compact keyring storage), the OAuth refresh is forced so the retry never re-sends the token the server just bounced. Static API tokens come back unchanged — the caller sees rejected == fresh and gives up.

func StateDir

func StateDir() string

StateDir returns the state directory path following XDG Base Directory spec (persistent but disposable data: history, logs). Typically ~/.local/state/dtctl.

func ValidateAliasName

func ValidateAliasName(name string) error

ValidateAliasName checks that an alias name is syntactically valid.

Types

type AliasEntry

type AliasEntry struct {
	Name      string `table:"NAME"`
	Expansion string `table:"EXPANSION"`
}

AliasEntry is a single alias for display purposes.

type AliasFile

type AliasFile struct {
	Aliases map[string]string `yaml:"aliases"`
}

AliasFile represents the YAML structure for import/export.

type CheckResult

type CheckResult struct {
	Allowed     bool
	Reason      string
	Suggestions []string
}

CheckResult contains the result of a safety check

type Checker

type Checker struct {
	// contains filtered or unexported fields
}

Checker performs safety level checks for operations

func NewChecker

func NewChecker(contextName string, ctx *Context) *Checker

NewChecker creates a new safety checker for a context

func NewCheckerWithLevel

func NewCheckerWithLevel(contextName string, level SafetyLevel) *Checker

NewCheckerWithLevel creates a new safety checker with an explicit safety level

func (*Checker) Check

func (c *Checker) Check(op Operation, ownership ResourceOwnership) CheckResult

Check verifies if an operation is allowed under the current safety level

func (*Checker) CheckError

func (c *Checker) CheckError(op Operation, ownership ResourceOwnership) error

CheckError performs a safety check and returns a *SafetyError if not allowed.

func (*Checker) ContextName

func (c *Checker) ContextName() string

ContextName returns the context name

func (*Checker) FormatError

func (c *Checker) FormatError(result CheckResult) string

FormatError formats a CheckResult as an error message

func (*Checker) SafetyLevel

func (c *Checker) SafetyLevel() SafetyLevel

SafetyLevel returns the current safety level

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the authenticated HTTP client for a Dynatrace environment.

func NewClient

func NewClient(baseURL, token string, opts ...ClientOption) (*Client, error)

NewClient creates a new client with base URL and token.

func NewClientFromConfig

func NewClientFromConfig(cfg *Config, opts ...ClientOption) (*Client, error)

NewClientFromConfig creates an authenticated client for the config's current context: OAuth-aware token resolution (keyring, file store, inline token), and re-resolution on 401 so long-running sessions survive OAuth access-token expiry.

func NewForTesting

func NewForTesting(baseURL, token string) (*Client, error)

NewForTesting creates a client with retries disabled, suitable for unit tests that use httptest servers. This avoids the 3×1s retry wait on 500/429 responses.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the base URL of the Dynatrace environment

func (*Client) CurrentUser

func (c *Client) CurrentUser() (*UserInfo, error)

CurrentUser fetches the current user info from the metadata API. Requires scope: app-engine:apps:run

func (*Client) CurrentUserID

func (c *Client) CurrentUserID() (string, error)

CurrentUserID returns the current user's ID. First tries the metadata API, falls back to JWT token decoding.

func (*Client) EnableTokenRefresh

func (c *Client) EnableTokenRefresh(resolve func(rejected string) (string, error))

EnableTokenRefresh registers a retry-on-401 hook: resolve is called with the rejected token and must return a fresh one (typically by refreshing an expired OAuth access token). The request is retried only when a genuinely new token was obtained; static tokens and failed refreshes surface the original 401. Safe for concurrent requests — one refresh serves them all.

resolve runs while tokenMu is held, so Token/SetToken and other 401 handling on this client block for its duration — bounded by the refresh lock timeout plus one token-endpoint round-trip. That serialization is deliberate (concurrent requests would only collect more 401s until the refresh lands); embedders should not call Token from a latency-sensitive loop while requests are in flight.

func (*Client) HTTP

func (c *Client) HTTP() *resty.Client

HTTP returns the underlying resty client

func (*Client) SetToken

func (c *Client) SetToken(token string)

SetToken updates the bearer token used for all subsequent HTTP requests. This is used to inject a freshly refreshed OAuth token without recreating the client.

func (*Client) SetVerbosity

func (c *Client) SetVerbosity(level int)

SetVerbosity sets the verbosity level for logging Level 0: normal (no debug output) Level 1: show request/response summary Level 2+: show full request/response details (sensitive headers always redacted)

func (*Client) Token

func (c *Client) Token() string

Token returns the bearer token currently used for requests.

type ClientOption

type ClientOption func(*clientOptions)

ClientOption customizes client construction.

func WithUserAgent

func WithUserAgent(ua string) ClientOption

WithUserAgent sets the full User-Agent product token (e.g. "dtctl/1.2.3"). The AI-agent-environment suffix is appended automatically.

func WithUserAgentProduct

func WithUserAgentProduct(product, version string) ClientOption

WithUserAgentProduct sets the User-Agent from a product name and version, e.g. WithUserAgentProduct("dtctl-foo", "0.5.0") → "dtctl-foo/0.5.0".

type Config

type Config struct {
	APIVersion     string            `yaml:"apiVersion"`
	Kind           string            `yaml:"kind"`
	CurrentContext string            `yaml:"current-context"`
	Contexts       []NamedContext    `yaml:"contexts"`
	Tokens         []NamedToken      `yaml:"tokens"`
	Preferences    Preferences       `yaml:"preferences"`
	Aliases        map[string]string `yaml:"aliases,omitempty"`
	// Spill holds the global result-spill settings (D15). Per-context overrides
	// live on Context.Spill.
	Spill SpillConfig `yaml:"spill,omitempty"`
	// Profiles is the set of named command profiles (default-deny allowlists of
	// commands). A profile is selected via DTCTL_PROFILE or a context binding;
	// see profile.go and docs/dev/COMMAND_PROFILES_DESIGN.md.
	Profiles map[string]Profile `yaml:"profiles,omitempty"`
	// contains filtered or unexported fields
}

Config represents the dtctl configuration

func Load

func Load() (*Config, error)

Load loads the configuration with the following precedence:

  1. Explicit config from the DTCTL_CONFIG environment variable (trusted)
  2. Local config (.dtctl.yaml in current directory or parent directories)
  3. Global config (XDG_CONFIG_HOME/dtctl/config)

If a local config is found, it is used exclusively (not merged with global).

Security: an auto-discovered local .dtctl.yaml is treated as untrusted (the classic "untrusted working directory / checked-out repo / shared dir" scenario). Code-execution keys — shell aliases and apply hooks — defined in such a config are never honored: alias resolution and hook execution check IsLocal() and skip them. These keys are honored only from the global config, an explicit --config file, or a config named by DTCTL_CONFIG (all loaded via LoadFrom without markLocal), which carry stronger ownership expectations. The keys are still loaded into the struct (and never mutated here) so that config-management commands round-trip the file without silently destroying a user's own aliases or hooks.

func LoadFrom

func LoadFrom(path string) (*Config, error)

LoadFrom loads the configuration from a specific path

func LoadFromWithoutExpansion

func LoadFromWithoutExpansion(path string) (*Config, error)

LoadFromWithoutExpansion loads the configuration from a specific path without expanding environment variables. Use this to inspect raw template values.

func LoadWithoutExpansion

func LoadWithoutExpansion() (*Config, error)

LoadWithoutExpansion loads the configuration without expanding environment variables, using the same search order as Load (local config, then global config).

func NewConfig

func NewConfig() *Config

NewConfig creates a new default configuration

func (*Config) CurrentContextObj

func (c *Config) CurrentContextObj() (*Context, error)

CurrentContextObj returns the current context object

func (*Config) DeleteAlias

func (c *Config) DeleteAlias(name string) error

DeleteAlias removes an alias by name. Returns an error if it does not exist.

func (*Config) DeleteContext

func (c *Config) DeleteContext(name string) error

DeleteContext removes a context by name. Returns an error if the context is not found.

func (*Config) EffectiveSpillConfig

func (c *Config) EffectiveSpillConfig() SpillConfig

EffectiveSpillConfig merges the global spill config with the current context's override (context wins per field, D15). Env and flag layers are applied by the caller on top of this base.

func (*Config) ExportAliases

func (c *Config) ExportAliases(path string) error

ExportAliases writes aliases to a file in YAML format.

func (*Config) GetAlias

func (c *Config) GetAlias(name string) (string, bool)

GetAlias returns the expansion for an alias, or empty string if not found.

func (*Config) GetContext

func (c *Config) GetContext(name string) (*NamedContext, error)

GetContext returns a named context by name

func (*Config) GetPostApplyHook

func (c *Config) GetPostApplyHook() string

GetPostApplyHook returns the effective post-apply hook command. Per-context hooks take precedence over global (preferences) hooks. The special value "none" explicitly disables the global hook for a context.

func (*Config) GetPreApplyHook

func (c *Config) GetPreApplyHook() string

GetPreApplyHook returns the effective pre-apply hook command. Per-context hooks take precedence over global (preferences) hooks. The special value "none" explicitly disables the global hook for a context.

func (*Config) GetToken

func (c *Config) GetToken(tokenRef string) (string, error)

GetToken retrieves a token by reference name. It first tries the OS keyring (checking both regular and OAuth tokens), then file-based OAuth token storage, then falls back to the config file.

func (*Config) IgnoredExecKeys

func (c *Config) IgnoredExecKeys() bool

IgnoredExecKeys reports whether code-execution keys (aliases, apply hooks) are present in the auto-discovered local config and are therefore ignored at runtime. See markLocal.

func (*Config) ImportAliases

func (c *Config) ImportAliases(path string, overwrite bool, builtinCheck func(string) bool) ([]string, error)

ImportAliases reads aliases from a YAML file and merges them into the config. If overwrite is false, existing aliases are not replaced and conflicts are returned as a list of names.

func (*Config) IsLocal

func (c *Config) IsLocal() bool

IsLocal reports whether the config was loaded from an auto-discovered local .dtctl.yaml (as opposed to the global config or an explicit --config file). Code-execution keys (aliases, apply hooks) are ignored when this is true.

func (*Config) ListAliases

func (c *Config) ListAliases() []AliasEntry

ListAliases returns all aliases sorted alphabetically by name.

func (*Config) LocalConfigPath

func (c *Config) LocalConfigPath() string

LocalConfigPath returns the path of the auto-discovered local .dtctl.yaml the config was loaded from, or "" if it was not loaded from a local config.

func (*Config) MustGetToken

func (c *Config) MustGetToken(tokenRef string) string

MustGetToken retrieves a token by reference name, returning empty string on error

func (*Config) ProfileExists

func (c *Config) ProfileExists(name string) bool

ProfileExists reports whether a profile name is resolvable — either "full", a user-defined profile, or a built-in preset. Used by config-management commands to warn on likely typos when binding a profile to a context.

func (*Config) PruneEmptyEnvironments

func (c *Config) PruneEmptyEnvironments(keepContext string, placeholderNames map[string]bool)

PruneEmptyEnvironments removes contexts whose names are in placeholderNames, except the named keepContext. Pass the context names from the raw (unexpanded) config file to avoid pruning contexts backed by currently-unset env vars.

func (*Config) ResolveProfile

func (c *Config) ResolveProfile() (*Profile, error)

ResolveProfile determines the active profile using the precedence

DTCTL_PROFILE env  >  context-bound profile  >  none (= full)

It returns nil for the full (unrestricted) command tree — the default and backward-compatible behavior. A referenced profile name that does not exist (as a user profile or built-in preset) is a fast, explicit error rather than a silent fallback to full, which would be a surprising surface expansion.

func (*Config) Save

func (c *Config) Save() error

Save saves the configuration to the default path

func (*Config) SaveTo

func (c *Config) SaveTo(path string) error

SaveTo saves the configuration to a specific path.

Contract rule (docs/dev/CONFIG_CONTRACT.md): fields unknown to this build — written by a newer dtctl or another schema-v1 writer — are grafted back from the file being overwritten, so an older writer never destroys them.

func (*Config) SetAlias

func (c *Config) SetAlias(name, expansion string, builtinCheck func(string) bool) error

SetAlias adds or updates an alias. Returns an error if the name is invalid. builtinCheck is called to verify the name does not shadow a built-in command.

func (*Config) SetContext

func (c *Config) SetContext(name, environment, tokenRef string)

SetContext creates or updates a context

func (*Config) SetContextWithOptions

func (c *Config) SetContextWithOptions(name, environment, tokenRef string, opts *ContextOptions)

SetContextWithOptions creates or updates a context with optional fields

func (*Config) SetToken

func (c *Config) SetToken(name, token string) error

SetToken creates or updates a token. If keyring is available, the token is stored securely in the OS keyring and only a reference is kept in the config file. Any cached OAuth tokens for this credential name are invalidated so that a rotated platform token does not keep using a stale refresh token.

type Context

type Context struct {
	Environment string      `yaml:"environment" table:"ENVIRONMENT"`
	TokenRef    string      `yaml:"token-ref" table:"TOKEN-REF"`
	SafetyLevel SafetyLevel `yaml:"safety-level,omitempty" table:"SAFETY-LEVEL"`
	Description string      `yaml:"description,omitempty" table:"DESCRIPTION,wide"`
	AccountUUID string      `yaml:"account-uuid,omitempty" table:"ACCOUNT-UUID,wide"`
	// Profile binds a command profile to this context, restricting the visible
	// command surface when the context is active (unless overridden by
	// DTCTL_PROFILE). Empty means the full command tree. See profile.go.
	Profile string `yaml:"profile,omitempty" table:"PROFILE,wide"`
	Hooks   Hooks  `yaml:"hooks,omitempty"`
	// Spill overrides the global spill settings for this context (D15). Nil
	// fields inherit the global spill config.
	Spill *SpillConfig `yaml:"spill,omitempty"`
}

Context holds the connection information for a Dynatrace environment

func (*Context) GetEffectiveSafetyLevel

func (c *Context) GetEffectiveSafetyLevel() SafetyLevel

GetEffectiveSafetyLevel returns the effective safety level for a context If no safety level is set, returns the default (readwrite-all)

type ContextOptions

type ContextOptions struct {
	SafetyLevel SafetyLevel
	Description string
	Profile     string
}

ContextOptions holds optional fields for context configuration

type Environment

type Environment string

Environment represents a Dynatrace environment type

const (
	EnvironmentProd Environment = "prod"
	EnvironmentDev  Environment = "dev"
	EnvironmentHard Environment = "hard"
)

func DetectEnvironment

func DetectEnvironment(environmentURL string) Environment

DetectEnvironment determines the environment type from a Dynatrace URL

type Hooks

type Hooks struct {
	PreApply  string `yaml:"pre-apply,omitempty"`
	PostApply string `yaml:"post-apply,omitempty"`
}

Hooks holds hook commands for lifecycle events

type NamedContext

type NamedContext struct {
	Name    string  `yaml:"name" table:"NAME"`
	Context Context `yaml:"context" table:"-"`
}

NamedContext holds a context with its name

type NamedToken

type NamedToken struct {
	Name  string `yaml:"name"`
	Token string `yaml:"token"`
}

NamedToken holds a token with its name

type OAuthConfig

type OAuthConfig struct {
	AuthURL     string
	TokenURL    string
	UserInfoURL string
	ClientID    string
	// Scopes are requested during the interactive login flow only; token
	// refresh re-issues the original grant's scopes, so refresh-only
	// consumers (the 401-retry path, TokenManager auto-refresh) may leave
	// them empty. The scope-composition tables live in dtctl's pkg/auth —
	// callers running a login flow pass the composed set in.
	Scopes         []string
	Port           int
	Environment    Environment
	SafetyLevel    SafetyLevel
	EnvironmentURL string
}

func DefaultOAuthConfig

func DefaultOAuthConfig() *OAuthConfig

DefaultOAuthConfig returns the default OAuth configuration for production. No scopes are set — sufficient for refresh-only use (see OAuthConfig.Scopes).

func OAuthConfigForEnvironment

func OAuthConfigForEnvironment(env Environment, safetyLevel SafetyLevel, scopes []string) *OAuthConfig

OAuthConfigForEnvironment creates an OAuth configuration for the specified environment. scopes may be nil for refresh-only use; login flows pass the scope set composed for the safety level (dtctl's pkg/auth owns that composition).

func OAuthConfigFromEnvironmentURL

func OAuthConfigFromEnvironmentURL(environmentURL string, safetyLevel SafetyLevel, scopes []string) *OAuthConfig

OAuthConfigFromEnvironmentURL creates an OAuth configuration by detecting the environment from a URL. scopes may be nil for refresh-only use.

type OAuthFileStore

type OAuthFileStore struct {
	// contains filtered or unexported fields
}

OAuthFileStore provides file-based storage for OAuth tokens. Tokens are stored as individual JSON files under $XDG_DATA_HOME/dtctl/oauth-tokens/, with 0600 permissions (owner-only read/write).

This is used as a fallback when the OS keyring is unavailable (e.g. headless Linux, WSL, CI/CD environments, containers).

func NewOAuthFileStore

func NewOAuthFileStore() *OAuthFileStore

NewOAuthFileStore creates a new file-based OAuth token store.

func NewOAuthFileStoreWithDir

func NewOAuthFileStoreWithDir(dir string) *OAuthFileStore

NewOAuthFileStoreWithDir creates a file store using a specific directory (for testing).

func (*OAuthFileStore) DeleteToken

func (fs *OAuthFileStore) DeleteToken(name string) error

DeleteToken removes a token file.

func (*OAuthFileStore) GetToken

func (fs *OAuthFileStore) GetToken(name string) (string, error)

GetToken reads a token from a file.

func (*OAuthFileStore) SetToken

func (fs *OAuthFileStore) SetToken(name, token string) error

SetToken writes a token to a file.

type OAuthFlow

type OAuthFlow struct {
	// contains filtered or unexported fields
}

func NewOAuthFlow

func NewOAuthFlow(config *OAuthConfig) (*OAuthFlow, error)

func (*OAuthFlow) GetUserInfo

func (f *OAuthFlow) GetUserInfo(accessToken string) (*OAuthUserInfo, error)

func (*OAuthFlow) RefreshToken

func (f *OAuthFlow) RefreshToken(refreshToken string) (*TokenSet, error)

func (*OAuthFlow) Start

func (f *OAuthFlow) Start(ctx context.Context) (*TokenSet, error)

type OAuthUserInfo

type OAuthUserInfo struct {
	Sub           string `json:"sub"`
	Email         string `json:"email"`
	EmailVerified bool   `json:"email_verified"`
	Name          string `json:"name"`
}

OAuthUserInfo is the SSO userinfo-endpoint response (distinct from the platform metadata UserInfo returned by Client.CurrentUser).

type Operation

type Operation string

Operation represents a type of operation that can be performed

const (
	// OperationRead is a read-only operation
	OperationRead Operation = "read"
	// OperationCreate is a create operation
	OperationCreate Operation = "create"
	// OperationUpdate is an update operation
	OperationUpdate Operation = "update"
	// OperationDelete is a delete operation
	OperationDelete Operation = "delete"
	// OperationDeleteBucket is a bucket deletion operation (data loss)
	OperationDeleteBucket Operation = "delete-bucket"
)

type Preferences

type Preferences struct {
	Output string `yaml:"output,omitempty"`
	Editor string `yaml:"editor,omitempty"`
	Hooks  Hooks  `yaml:"hooks,omitempty"`
}

Preferences holds user preferences

type Profile

type Profile struct {
	// Name is the profile's key in the profiles map (or the built-in preset
	// name). It is populated at resolution time and never serialized — the map
	// key already carries it.
	Name string `yaml:"-"`
	// Description is a human-readable summary of the profile's purpose.
	Description string `yaml:"description,omitempty"`
	// Commands is the allowlist of command-path prefixes. An entry matches a
	// command when it equals or is a segment-prefix of that command's path, so
	// listing a parent verb (e.g. "describe") includes its whole subtree. There
	// is no denylist — everything not matched (and not always-available) is masked.
	Commands []string `yaml:"commands,omitempty"`
}

Profile is a default-deny allowlist of commands. It shapes every discovery surface at once (--help, `dtctl commands`, shell completion) and hard-blocks invocation of any command outside the set. See docs/dev/COMMAND_PROFILES_DESIGN.md.

func (*Profile) Allows

func (p *Profile) Allows(path string) bool

Allows reports whether a command identified by its space-joined path (relative to the root command, e.g. "get workflows") is visible/runnable under this profile. A command is allowed when:

  1. it is in the always-available set, or
  2. it is at or below an allowlisted entry (the entry is a segment-prefix of the path — listing "describe" allows "describe analyzer"), or
  3. it is an ancestor of an allowlisted entry (the path is a segment-prefix of the entry — a parent verb stays reachable so its allowed child can be run).

Everything else is masked.

type ResourceOwnership

type ResourceOwnership int

ResourceOwnership indicates whether a resource is owned by the current user

const (
	// OwnershipUnknown means ownership cannot be determined
	OwnershipUnknown ResourceOwnership = iota
	// OwnershipOwn means the resource is owned by the current user
	OwnershipOwn
	// OwnershipShared means the resource is owned by someone else
	OwnershipShared
)

func DetermineOwnership

func DetermineOwnership(resourceOwnerID, currentUserID string) ResourceOwnership

DetermineOwnership compares resource owner with current user ID to determine ownership. Returns OwnershipOwn if they match, OwnershipShared if they don't, or OwnershipUnknown if either value is empty.

type SafetyError

type SafetyError struct {
	ContextName string
	SafetyLevel SafetyLevel
	Operation   Operation
	Reason      string
	Suggestions []string
}

SafetyError represents a safety check failure

func (*SafetyError) Error

func (e *SafetyError) Error() string

type SafetyLevel

type SafetyLevel string

SafetyLevel defines the allowed operations for a context

const (
	// SafetyLevelReadOnly allows only read operations
	SafetyLevelReadOnly SafetyLevel = "readonly"
	// SafetyLevelReadWriteMine allows create/update/delete of own resources only
	SafetyLevelReadWriteMine SafetyLevel = "readwrite-mine"
	// SafetyLevelReadWriteAll allows modification of all resources (no bucket deletion)
	SafetyLevelReadWriteAll SafetyLevel = "readwrite-all"
	// SafetyLevelDangerouslyUnrestricted allows all operations including data deletion
	SafetyLevelDangerouslyUnrestricted SafetyLevel = "dangerously-unrestricted"

	// DefaultSafetyLevel is used when no safety level is specified.
	// We use readwrite-all as default to avoid breaking existing workflows.
	// This allows all operations except bucket deletion, which is the most
	// common use case and matches pre-safety-level behavior.
	DefaultSafetyLevel = SafetyLevelReadWriteAll
)

func ValidSafetyLevels

func ValidSafetyLevels() []SafetyLevel

ValidSafetyLevels returns all valid safety level values

func (SafetyLevel) IsValid

func (s SafetyLevel) IsValid() bool

IsValid checks if the safety level is valid

func (SafetyLevel) String

func (s SafetyLevel) String() string

String returns the string representation of the safety level

type SpillConfig

type SpillConfig struct {
	Mode      string `yaml:"mode,omitempty"`      // auto|always|never
	Dir       string `yaml:"dir,omitempty"`       // base directory for spilled files
	Format    string `yaml:"format,omitempty"`    // jsonl|json|csv|parquet (default jsonl)
	Threshold string `yaml:"threshold,omitempty"` // e.g. "50KB"
	TTL       string `yaml:"ttl,omitempty"`       // e.g. "24h"
}

SpillConfig holds the result-spill settings (D15). Threshold and TTL are kept as human-friendly strings in the file (e.g. "50KB", "24h") and parsed when resolving the effective settings. All fields are optional; an unset field inherits from the next layer in the precedence chain (flag → env → context-config → global-config → built-in default).

type StoredToken

type StoredToken struct {
	TokenSet
	Name string `json:"name"`
}

StoredToken represents a stored OAuth token set

type TokenManager

type TokenManager struct {
	// contains filtered or unexported fields
}

TokenManager manages OAuth tokens including storage and refresh

func NewTokenManager

func NewTokenManager(oauthConfig *OAuthConfig) (*TokenManager, error)

NewTokenManager creates a new token manager

func (*TokenManager) CachedScopes

func (tm *TokenManager) CachedScopes(tokenName string) []string

CachedScopes returns the granted scopes preserved in the scope-companion keyring entry, or nil when there is none. It is used as a fallback for scope display when the primary token entry dropped the scope string to fit size limits. Only consulted for keyring storage; the file store keeps the full token (scope included), so no companion is needed there.

func (*TokenManager) DeleteToken

func (tm *TokenManager) DeleteToken(tokenName string) error

DeleteToken removes a stored OAuth token

func (*TokenManager) GetToken

func (tm *TokenManager) GetToken(tokenName string) (string, error)

GetToken retrieves and optionally refreshes a token.

When multiple processes run in parallel (e.g. concurrent dtctl invocations) they may all see a compact token (no access_token) or an about-to-expire token and all attempt to refresh simultaneously. Because OAuth uses refresh token rotation, only the first refresh succeeds; the others receive "invalid_grant". To prevent this, we acquire a cross-process advisory lock before any refresh and re-read the token after acquiring it, so that the 2nd+ processes reuse the access_token the first one wrote.

func (*TokenManager) GetTokenInfo

func (tm *TokenManager) GetTokenInfo(tokenName string) (*StoredToken, error)

GetTokenInfo retrieves information about a stored OAuth token

func (*TokenManager) RefreshToken

func (tm *TokenManager) RefreshToken(tokenName string) (*TokenSet, error)

RefreshToken forces a refresh of an OAuth token by exchanging the stored refresh token for a new token set and persisting the result.

The refresh runs under the same cross-process lock GetToken uses: OAuth refresh-token rotation invalidates a refresh token after first use, so an unguarded forced refresh (e.g. a long-running consumer reacting to a 401 while a parallel dtctl invocation refreshes on expiry) would strand one side with "invalid_grant". After acquiring the lock the token is re-read — if another process refreshed while we waited (the stored access token changed and is not near expiry), that fresher token set is returned instead of spending another refresh. A genuinely-forced refresh (nothing changed while waiting) always proceeds, so a token the server rejects despite looking valid can never be returned to the caller unrefreshed. Like GetToken, the lock is best-effort: on lock failure a warning is printed and the refresh proceeds unguarded.

func (*TokenManager) SaveToken

func (tm *TokenManager) SaveToken(tokenName string, tokens *TokenSet) error

SaveToken stores an OAuth token set

func (*TokenManager) SetWarnFunc

func (tm *TokenManager) SetWarnFunc(fn func(format string, args ...any))

SetWarnFunc replaces the destination for non-fatal warnings (e.g. a failed best-effort refresh-lock acquisition). The default writes to stderr, which suits a CLI; embedders that own the terminal (a TUI like dynatui) should route warnings into their own surface instead. A nil fn silences warnings.

type TokenSet

type TokenSet struct {
	AccessToken  string    `json:"access_token"`
	RefreshToken string    `json:"refresh_token"`
	IDToken      string    `json:"id_token"`
	TokenType    string    `json:"token_type"`
	ExpiresIn    int       `json:"expires_in"`
	Scope        string    `json:"scope"`
	ExpiresAt    time.Time `json:"expires_at,omitempty"`
}

type TokenStore

type TokenStore struct {
	// contains filtered or unexported fields
}

TokenStore provides secure token storage using the OS keyring

func NewTokenStore

func NewTokenStore() *TokenStore

NewTokenStore creates a new token store

func (*TokenStore) DeleteToken

func (ts *TokenStore) DeleteToken(name string) error

DeleteToken removes a token from the OS keyring

func (*TokenStore) GetToken

func (ts *TokenStore) GetToken(name string) (string, error)

GetToken retrieves a token from the OS keyring

func (*TokenStore) SetToken

func (ts *TokenStore) SetToken(name, token string) error

SetToken stores a token securely in the OS keyring

type UserInfo

type UserInfo struct {
	UserName     string `json:"userName"`
	UserID       string `json:"userId"`
	EmailAddress string `json:"emailAddress"`
}

UserInfo contains information about the current user

Jump to

Keyboard shortcuts

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