Conversation
Make the token writing interface support any type.
… sensitive information Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (5)
internal/auth/identities/aws/assume_role.go (1)
165-175: Honor interface: return via.provider only; don’t fall back to via.identity.The Identity contract says non-user identities return via.provider. Returning via.identity violates it and can break consumers.
func (i *assumeRoleIdentity) GetProviderName() (string, error) { if i.config.Via != nil && i.config.Via.Provider != "" { return i.config.Via.Provider, nil } - if i.config.Via != nil && i.config.Via.Identity != "" { - // This assume role identity chains through another identity. - // For caching purposes, we'll use the chained identity name. - return i.config.Via.Identity, nil - } - return "", fmt.Errorf("%w: assume role identity %q has no valid via configuration", errUtils.ErrInvalidIdentityConfig, i.name) + return "", fmt.Errorf("%w: assume role identity %q has no valid via.provider configuration", errUtils.ErrInvalidIdentityConfig, i.name) }tests/fixtures/scenarios/atmos-auth/atmos.yaml (1)
10-21: Ensure schemas validate new auth keys (providers/identities).We need JSON Schemas to expose/validate
auth.providersandauth.identitiesso this fixture passes validation. Prior reviews flagged this already.Run to confirm coverage and identify files to update:
#!/bin/bash set -euo pipefail echo "Search Go types (expect Auth/Provider/Identity present):" rg -nP 'type\s+(Auth|AuthConfig|Provider|Identity)\b' -g '!**/vendor/**' || true echo echo "Check JSON Schemas expose 'auth' at root and define providers/identities:" fd -H -t f '1.0.json' pkg/datafetcher/schema | while read -r f; do echo "== $f" rg -nC2 '"auth"' "$f" || true rg -nC2 '"providers"' "$f" || true rg -nC2 '"identities"' "$f" || true doneHappy to draft the schema additions (root
authproperty +definitions.auth/providers/identities).internal/auth/providers/aws/saml.go (3)
264-277: Use case-insensitive detection and return “GoogleApps” for Google Workspace.saml2aws expects Provider="GoogleApps" for Google Workspace SAML; also normalize URL checks.
- // Auto-detect provider type based on URL - if strings.Contains(p.url, "accounts.google.com") { - return "Browser" // Use Browser for Google Apps SAML for better compatibility - } - if strings.Contains(p.url, "okta.com") { + // Auto-detect provider type based on URL. + u := strings.ToLower(p.url) + if strings.Contains(u, "accounts.google.com") { + return "GoogleApps" + } + if strings.Contains(u, "okta.com") { return "Okta" } - if strings.Contains(p.url, "adfs") { + if strings.Contains(u, "adfs") { return "ADFS" }Please confirm the current provider strings in versent/saml2aws v2:
For versent/saml2aws v2, what are the valid values for cfg.IDPAccount.Provider and the correct value for Google Workspace SAML?
106-109: Ensure saml2aws config dir exists before client/auth.Create ~/.saml2aws proactively to prevent first-run failures. This was raised previously.
// Create config and client + login details samlConfig := p.createSAMLConfig() + if err := ensureSAML2AWSConfigDir(); err != nil { + return nil, fmt.Errorf("%w: failed to ensure saml2aws config dir: %v", errUtils.ErrInvalidProviderConfig, err) + } loginDetails := p.createLoginDetails()Add this helper in the same file:
// add to imports: // "path/filepath" func ensureSAML2AWSConfigDir() error { home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("%w: get user home directory: %v", errUtils.ErrInvalidProviderConfig, err) } dir := filepath.Join(home, ".saml2aws") if err := os.MkdirAll(dir, 0o700); err != nil { return fmt.Errorf("%w: create saml2aws config directory: %v", errUtils.ErrInvalidProviderConfig, err) } return nil }
241-253: Prevent nil deref on STS response.STS can return a nil result or nil Credentials on error paths. Guard before dereferencing.
result, err := stsClient.AssumeRoleWithSAML(ctx, input) if err != nil { return nil, fmt.Errorf("%w: failed to assume role with SAML: %v", errUtils.ErrAuthenticationFailed, err) } +if result == nil || result.Credentials == nil { + return nil, fmt.Errorf("%w: STS returned empty credentials", errUtils.ErrAuthenticationFailed) +} // Convert to AWSCredentials creds := &types.AWSCredentials{ - AccessKeyID: aws.ToString(result.Credentials.AccessKeyId), - SecretAccessKey: aws.ToString(result.Credentials.SecretAccessKey), - SessionToken: aws.ToString(result.Credentials.SessionToken), + AccessKeyID: aws.ToString(result.Credentials.AccessKeyId), + SecretAccessKey: aws.ToString(result.Credentials.SecretAccessKey), + SessionToken: aws.ToString(result.Credentials.SessionToken), Region: p.region, Expiration: result.Credentials.Expiration.Format(time.RFC3339), }
🧹 Nitpick comments (7)
internal/auth/identities/aws/assume_role.go (3)
73-79: Validate and bound DurationSeconds to STS limits (900–43200s).Prevents negatives or out-of-range values; keeps server-side errors out of the happy path.
if durationStr, ok := i.config.Principal["duration"].(string); ok && durationStr != "" { - if duration, err := time.ParseDuration(durationStr); err == nil { - input.DurationSeconds = aws.Int32(int32(duration.Seconds())) - } else { - log.Warn("Invalid duration specified for assume role", "duration", durationStr) - } + if duration, err := time.ParseDuration(durationStr); err == nil { + secs := int32(duration.Seconds()) + if secs < 0 { + log.Warn("Negative duration specified for assume role; ignoring.", "duration", durationStr) + } else { + if secs < 900 { + secs = 900 + } else if secs > 43200 { + secs = 43200 + } + input.DurationSeconds = aws.Int32(secs) + } + } else { + log.Warn("Invalid duration specified for assume role.", "duration", durationStr) + } }
65-69: Truncate session name to 64 chars to satisfy STS constraints.AWS STS enforces a 64‑char max for RoleSessionName.
- sessionName := fmt.Sprintf("atmos-%s-%d", i.name, time.Now().Unix()) + sessionName := fmt.Sprintf("atmos-%s-%d", i.name, time.Now().Unix()) + if len(sessionName) > 64 { + sessionName = sessionName[:64] + }
27-42: DRY region fallback logic.With the Authenticate change pinning i.region, newSTSClient doesn’t need its own fallback; it can assume i.region is set. Optional cleanup.
func (i *assumeRoleIdentity) newSTSClient(ctx context.Context, awsBase *types.AWSCredentials) (*sts.Client, error) { - region := i.region - if region == "" { - region = awsBase.Region - } - if region == "" { - region = "us-east-1" - } - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(i.region)) if err != nil { return nil, fmt.Errorf("%w: failed to load AWS config: %v", errUtils.ErrInvalidIdentityConfig, err) }tests/fixtures/scenarios/atmos-auth/atmos.yaml (1)
12-14: Double-check GitHub OIDC provider region support
I didn’t find any code handling aregionfield for thegithub/oidcprovider; verify whether it’s used—remove it if unused or document its purpose.internal/auth/providers/aws/saml.go (3)
98-101: Do not log IdP URLs at info level.Reduce exposure; URL can contain tenant details. Move to debug and drop URL.
- log.Info("Starting SAML authentication", "provider", p.name, "url", p.url) + log.Debug("Starting SAML authentication.", "provider", p.name)
12-12: Avoid import alias shadowing the local package name.Rename the AWS SDK alias to reduce cognitive load.
-import ( - aws "github.com/aws/aws-sdk-go-v2/aws" +import ( + awsv2 "github.com/aws/aws-sdk-go-v2/aws"- input := &sts.AssumeRoleWithSAMLInput{ - RoleArn: aws.String(role.RoleARN), - PrincipalArn: aws.String(role.PrincipalARN), - SAMLAssertion: aws.String(samlAssertion), - DurationSeconds: aws.Int32(func() int32 { + input := &sts.AssumeRoleWithSAMLInput{ + RoleArn: awsv2.String(role.RoleARN), + PrincipalArn: awsv2.String(role.PrincipalARN), + SAMLAssertion: awsv2.String(samlAssertion), + DurationSeconds: awsv2.Int32(func() int32 { // Respect requested duration within STS/account limits. if p.config != nil && p.config.Session != nil && p.config.Session.Duration != "" { if duration, err := time.ParseDuration(p.config.Session.Duration); err == nil { return int32(duration.Seconds()) } } return 3600 }()), }- AccessKeyID: aws.ToString(result.Credentials.AccessKeyId), - SecretAccessKey: aws.ToString(result.Credentials.SecretAccessKey), - SessionToken: aws.ToString(result.Credentials.SessionToken), + AccessKeyID: awsv2.ToString(result.Credentials.AccessKeyId), + SecretAccessKey: awsv2.ToString(result.Credentials.SecretAccessKey), + SessionToken: awsv2.ToString(result.Credentials.SessionToken),Also applies to: 227-239, 248-253
151-167: Abstract external clients behind interfaces for testability.Constructing saml2aws and STS clients inline makes unit testing harder. Consider injecting factories/interfaces.
Happy to sketch minimal interfaces and a provider-level struct field to accept test doubles.
Also applies to: 215-224
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
examples/demo-auth/atmos.yaml(1 hunks)internal/auth/identities/aws/assume_role.go(1 hunks)internal/auth/providers/aws/assume_role.go(1 hunks)internal/auth/providers/aws/saml.go(1 hunks)internal/auth/providers/github/oidc_test.go(1 hunks)tests/fixtures/scenarios/atmos-auth/atmos.yaml(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- examples/demo-auth/atmos.yaml
- internal/auth/providers/github/oidc_test.go
- internal/auth/providers/aws/assume_role.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)
**/*.go: Use Viper for managing configuration, environment variables, and flags
Use interfaces for external dependencies to facilitate mocking
All code must pass golangci-lint checks
Follow Go's error handling idioms
Use meaningful error messages
Wrap errors with context using fmt.Errorf("context: %w", err)
Consider using a custom error type for domain-specific errors
Follow standard Go coding style
Use gofmt and goimports to format code
Prefer short, descriptive variable names
Use snake_case for environment variables
Document all exported functions, types, and methods
Document complex logic with inline comments
Follow Go's documentation conventions
Use Viper for configuration management
Support configuration via files, environment variables, and flags
Follow the precedence order: flags > environment variables > config file > defaults
**/*.go: All comments in Go code must end with periods (enforced by golangci-lint godot).
Wrap all returned errors using static errors from the errors package; never return dynamic errors directly.
Always bind environment variables with viper.BindEnv() and provide ATMOS_ alternatives for each external var.
Separate structured logging from UI output: use stderr for prompts/errors to user; stdout only for data; never use logging for UI.
Most text UI must go to stderr; only data/results to stdout. Prefer utils.PrintfMessageToTUI for UI messages.
For non-standard execution paths, capture telemetry with telemetry.CaptureCmd or telemetry.CaptureCmdString and never capture user data.
Ensure cross-platform compatibility: prefer SDKs to external binaries, use filepath/os/runtime for portability, and add build constraints when needed.
Files:
internal/auth/identities/aws/assume_role.gointernal/auth/providers/aws/saml.go
**/*.{yaml,yml}
📄 CodeRabbit inference engine (CLAUDE.md)
YAML configs should support Go templating using provided template functions.
Files:
tests/fixtures/scenarios/atmos-auth/atmos.yaml
🧬 Code graph analysis (2)
internal/auth/identities/aws/assume_role.go (5)
internal/auth/types/interfaces.go (3)
Identity(75-95)ICredentials(171-177)Provider(53-72)pkg/schema/schema.go (5)
Identity(1007-1015)Context(371-386)Validate(184-186)Provider(987-999)ConfigAndStacksInfo(460-539)internal/auth/types/structs.go (1)
AWSCredentials(6-13)errors/errors.go (4)
ErrInvalidIdentityConfig(85-85)ErrAuthenticationFailed(88-88)ErrInvalidIdentityKind(84-84)ErrAwsAuth(92-92)internal/auth/cloud/aws/setup.go (2)
SetupFiles(13-38)SetEnvironmentVariables(41-50)
internal/auth/providers/aws/saml.go (4)
internal/auth/types/interfaces.go (3)
Provider(53-72)AuthManager(98-136)ICredentials(171-177)pkg/schema/schema.go (3)
Provider(987-999)Context(371-386)Validate(184-186)errors/errors.go (6)
ErrInvalidProviderKind(86-86)ErrInvalidProviderConfig(87-87)ErrInvalidAuthConfig(83-83)ErrInvalidIdentityConfig(85-85)ErrAwsSAMLDecodeFailed(94-94)ErrAuthenticationFailed(88-88)internal/auth/types/structs.go (1)
AWSCredentials(6-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build (windows-latest, windows)
- GitHub Check: Summary
🔇 Additional comments (5)
internal/auth/identities/aws/assume_role.go (2)
177-187: LGTM: post-auth file/env setup.Errors are wrapped with ErrAwsAuth and no secrets are logged. Matches guidelines.
13-17: Remove incorrect errUtils path suggestion
Theerrorspackage is defined inerrors/errors.go, so importinggithub.com/cloudposse/atmos/errors(aliased aserrUtils) is correct.Likely an incorrect or invalid review comment.
tests/fixtures/scenarios/atmos-auth/atmos.yaml (3)
22-23: Ignore log level casing suggestion;Debugmatches the defined enum.Likely an incorrect or invalid review comment.
9-9: Confirmed{stage}-only stack name pattern is supported — the{stage}token is used in existing config tests (config_test.go) and all resolvers accept it without requiring additional tokens.
20-21: Ignore incorrect field-name suggestion:assume_roleis correct.
Theaws/assume-roleidentity’s Validate method requiresprincipal.assume_roleand setsRoleArnfor the STS call—role_arnisn’t supported in config.Likely an incorrect or invalid review comment.
| return &types.AWSCredentials{ | ||
| AccessKeyID: aws.ToString(result.Credentials.AccessKeyId), | ||
| SecretAccessKey: aws.ToString(result.Credentials.SecretAccessKey), | ||
| SessionToken: aws.ToString(result.Credentials.SessionToken), | ||
| Region: i.region, | ||
| Expiration: expiration, | ||
| }, nil |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Fix region propagation so returned creds/config use the same region STS used.
toAWSCredentials sets Region from i.region, but newSTSClient may derive a different fallback (base creds or us-east-1). This can write AWS config with the wrong region. Pin the effective region in Authenticate before creating the STS client.
func (i *assumeRoleIdentity) Authenticate(ctx context.Context, baseCreds types.ICredentials) (types.ICredentials, error) {
@@
- // Create STS client with base credentials.
- stsClient, err := i.newSTSClient(ctx, awsBase)
+ // Determine and pin the effective region so downstream uses the same value.
+ effectiveRegion := i.region
+ if effectiveRegion == "" {
+ effectiveRegion = awsBase.Region
+ }
+ if effectiveRegion == "" {
+ effectiveRegion = "us-east-1"
+ }
+ i.region = effectiveRegion
+ // Create STS client with base credentials.
+ stsClient, err := i.newSTSClient(ctx, awsBase)
if err != nil {
return nil, err
}Also applies to: 114-121
🤖 Prompt for AI Agents
In internal/auth/identities/aws/assume_role.go around lines 53-59 (and similarly
lines 114-121), the returned AWSCredentials use i.region while newSTSClient may
have resolved a different effective region (from base creds or defaulting to
us-east-1); before creating the STS client in Authenticate, determine and pin
the effective region value that the STS client will use and then pass that
pinned region into both newSTSClient and into the Region field of the returned
types.AWSCredentials so the credentials/config reflect the same region STS
actually used.
| func NewAssumeRoleIdentity(name string, config *schema.Identity) (types.Identity, error) { | ||
| if config.Kind != "aws/assume-role" { | ||
| return nil, fmt.Errorf("%w: invalid identity kind for assume role: %s", errUtils.ErrInvalidIdentityKind, config.Kind) | ||
| } | ||
|
|
||
| return &assumeRoleIdentity{ | ||
| name: name, | ||
| config: config, | ||
| }, nil | ||
| } |
There was a problem hiding this comment.
Guard against nil config.
NewAssumeRoleIdentity dereferences config.Kind without a nil check.
func NewAssumeRoleIdentity(name string, config *schema.Identity) (types.Identity, error) {
+ if config == nil {
+ return nil, fmt.Errorf("%w: identity config is nil", errUtils.ErrInvalidIdentityConfig)
+ }
if config.Kind != "aws/assume-role" {
return nil, fmt.Errorf("%w: invalid identity kind for assume role: %s", errUtils.ErrInvalidIdentityKind, config.Kind)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func NewAssumeRoleIdentity(name string, config *schema.Identity) (types.Identity, error) { | |
| if config.Kind != "aws/assume-role" { | |
| return nil, fmt.Errorf("%w: invalid identity kind for assume role: %s", errUtils.ErrInvalidIdentityKind, config.Kind) | |
| } | |
| return &assumeRoleIdentity{ | |
| name: name, | |
| config: config, | |
| }, nil | |
| } | |
| func NewAssumeRoleIdentity(name string, config *schema.Identity) (types.Identity, error) { | |
| if config == nil { | |
| return nil, fmt.Errorf("%w: identity config is nil", errUtils.ErrInvalidIdentityConfig) | |
| } | |
| if config.Kind != "aws/assume-role" { | |
| return nil, fmt.Errorf("%w: invalid identity kind for assume role: %s", errUtils.ErrInvalidIdentityKind, config.Kind) | |
| } | |
| return &assumeRoleIdentity{ | |
| name: name, | |
| config: config, | |
| }, nil | |
| } |
🤖 Prompt for AI Agents
In internal/auth/identities/aws/assume_role.go around lines 84 to 93,
NewAssumeRoleIdentity dereferences config.Kind without checking config for nil;
add a nil guard at the start (e.g., if config == nil) and return a descriptive
error (use an appropriate error variable or fmt.Errorf) before accessing fields,
then proceed with the existing kind check and return the constructed
assumeRoleIdentity.
| // selectRole selects the AWS role (first for now, with logging). | ||
| func (p *samlProvider) selectRole(awsRoles []*saml2aws.AWSRole) *saml2aws.AWSRole { | ||
| // Use the first role or let user select if multiple | ||
| // If we have a preferred hint, try to match it | ||
| hint := strings.ToLower(p.RoleToAssumeFromAssertion) | ||
| for _, r := range awsRoles { | ||
| if strings.Contains(strings.ToLower(r.RoleARN), hint) || strings.Contains(strings.ToLower(r.PrincipalARN), hint) { | ||
| log.Debug("Selecting role matching preferred hint", "role", r.RoleARN, "hint", p.RoleToAssumeFromAssertion) | ||
| return r | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Select a safe default role (single-role/exact match) instead of failing.
Avoid unnecessary failures when the assertion yields exactly one role or an exact ARN match.
func (p *samlProvider) selectRole(awsRoles []*saml2aws.AWSRole) *saml2aws.AWSRole {
- // Use the first role or let user select if multiple
- // If we have a preferred hint, try to match it
+ // If only one role is available, select it; otherwise, try to match the preferred hint.
+ if len(awsRoles) == 1 {
+ log.Debug("Selecting only available role.", "role", awsRoles[0].RoleARN)
+ return awsRoles[0]
+ }
+
hint := strings.ToLower(p.RoleToAssumeFromAssertion)
+ // Prefer exact match first.
+ for _, r := range awsRoles {
+ if strings.EqualFold(r.RoleARN, p.RoleToAssumeFromAssertion) || strings.EqualFold(r.PrincipalARN, p.RoleToAssumeFromAssertion) {
+ log.Debug("Selecting role matching preferred hint (exact).", "role", r.RoleARN)
+ return r
+ }
+ }
for _, r := range awsRoles {
if strings.Contains(strings.ToLower(r.RoleARN), hint) || strings.Contains(strings.ToLower(r.PrincipalARN), hint) {
- log.Debug("Selecting role matching preferred hint", "role", r.RoleARN, "hint", p.RoleToAssumeFromAssertion)
+ log.Debug("Selecting role matching preferred hint (substring).", "role", r.RoleARN, "hint", p.RoleToAssumeFromAssertion)
return r
}
}
- return nil
+ log.Debug("No role matched preferred hint; multiple roles present; not selecting by default.")
+ return nil
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // selectRole selects the AWS role (first for now, with logging). | |
| func (p *samlProvider) selectRole(awsRoles []*saml2aws.AWSRole) *saml2aws.AWSRole { | |
| // Use the first role or let user select if multiple | |
| // If we have a preferred hint, try to match it | |
| hint := strings.ToLower(p.RoleToAssumeFromAssertion) | |
| for _, r := range awsRoles { | |
| if strings.Contains(strings.ToLower(r.RoleARN), hint) || strings.Contains(strings.ToLower(r.PrincipalARN), hint) { | |
| log.Debug("Selecting role matching preferred hint", "role", r.RoleARN, "hint", p.RoleToAssumeFromAssertion) | |
| return r | |
| } | |
| } | |
| return nil | |
| } | |
| // selectRole selects the AWS role (first for now, with logging). | |
| func (p *samlProvider) selectRole(awsRoles []*saml2aws.AWSRole) *saml2aws.AWSRole { | |
| // If only one role is available, select it; otherwise, try to match the preferred hint. | |
| if len(awsRoles) == 1 { | |
| log.Debug("Selecting only available role.", "role", awsRoles[0].RoleARN) | |
| return awsRoles[0] | |
| } | |
| hint := strings.ToLower(p.RoleToAssumeFromAssertion) | |
| // Prefer exact match first. | |
| for _, r := range awsRoles { | |
| if strings.EqualFold(r.RoleARN, p.RoleToAssumeFromAssertion) || strings.EqualFold(r.PrincipalARN, p.RoleToAssumeFromAssertion) { | |
| log.Debug("Selecting role matching preferred hint (exact).", "role", r.RoleARN) | |
| return r | |
| } | |
| } | |
| // Next, try substring matches against the hint. | |
| for _, r := range awsRoles { | |
| if strings.Contains(strings.ToLower(r.RoleARN), hint) || strings.Contains(strings.ToLower(r.PrincipalARN), hint) { | |
| log.Debug("Selecting role matching preferred hint (substring).", "role", r.RoleARN, "hint", p.RoleToAssumeFromAssertion) | |
| return r | |
| } | |
| } | |
| log.Debug("No role matched preferred hint; multiple roles present; not selecting by default.") | |
| return nil | |
| } |
🤖 Prompt for AI Agents
In internal/auth/providers/aws/saml.go around lines 200 to 213, the selectRole
function currently only returns a role when a preferred hint is contained in an
ARN and otherwise returns nil; update it to be more resilient by: 1) if awsRoles
has exactly one entry, immediately return that role; 2) normalize the hint and
try to find an exact case-insensitive match against RoleARN or PrincipalARN
first; 3) if no exact match, keep the existing contains-based matching; and 4)
if still no match, return the first role as a safe default instead of nil so the
caller can proceed.
| // setupBrowserAutomation sets up browser automation for SAML authentication. | ||
| func (p *samlProvider) setupBrowserAutomation() { | ||
| // Set environment variables for browser automation | ||
| if p.config.DownloadBrowserDriver { | ||
| os.Setenv("SAML2AWS_AUTO_BROWSER_DOWNLOAD", "true") | ||
| } | ||
|
|
||
| // For Google Apps SAML, we need to use Browser provider type | ||
| if strings.Contains(p.url, "accounts.google.com") { | ||
| log.Debug("Detected Google Apps SAML, using Browser provider") | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Avoid process-wide env mutation in library code; rely on Environment().
Setting env vars via os.Setenv here introduces side effects; return them from Environment() and let the caller apply.
func (p *samlProvider) setupBrowserAutomation() {
- // Set environment variables for browser automation
- if p.config.DownloadBrowserDriver {
- os.Setenv("SAML2AWS_AUTO_BROWSER_DOWNLOAD", "true")
- }
+ // Environment variables for browser automation are returned via Environment(); avoid process-wide side effects here.
- // For Google Apps SAML, we need to use Browser provider type
- if strings.Contains(p.url, "accounts.google.com") {
- log.Debug("Detected Google Apps SAML, using Browser provider")
+ // For Google Apps SAML, adjust provider selection via getProviderType().
+ if strings.Contains(strings.ToLower(p.url), "accounts.google.com") {
+ log.Debug("Detected Google Apps SAML; provider selection will be auto-detected.")
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // setupBrowserAutomation sets up browser automation for SAML authentication. | |
| func (p *samlProvider) setupBrowserAutomation() { | |
| // Set environment variables for browser automation | |
| if p.config.DownloadBrowserDriver { | |
| os.Setenv("SAML2AWS_AUTO_BROWSER_DOWNLOAD", "true") | |
| } | |
| // For Google Apps SAML, we need to use Browser provider type | |
| if strings.Contains(p.url, "accounts.google.com") { | |
| log.Debug("Detected Google Apps SAML, using Browser provider") | |
| } | |
| } | |
| // setupBrowserAutomation sets up browser automation for SAML authentication. | |
| func (p *samlProvider) setupBrowserAutomation() { | |
| // Environment variables for browser automation are returned via Environment(); avoid process-wide side effects here. | |
| // For Google Apps SAML, adjust provider selection via getProviderType(). | |
| if strings.Contains(strings.ToLower(p.url), "accounts.google.com") { | |
| log.Debug("Detected Google Apps SAML; provider selection will be auto-detected.") | |
| } | |
| } |
🤖 Prompt for AI Agents
internal/auth/providers/aws/saml.go lines 313-324: setupBrowserAutomation
currently uses os.Setenv which mutates process-wide environment; instead remove
os.Setenv calls and have the function populate and return (or add to the
provider's existing Environment map/struct) the key
"SAML2AWS_AUTO_BROWSER_DOWNLOAD" set to "true" when
p.config.DownloadBrowserDriver is true so callers can apply it; keep the Google
Apps detection/logging but do not set any global env vars in library code.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (15)
tests/fixtures/scenarios/auth/atmos.yaml (1)
65-71: Schema path must exist and auth schemas must be defined.The manifest path may be missing, and auth.providers/identities need JSON Schema coverage. This was flagged before; verify and add schemas or update the path.
#!/bin/bash # Verify schemas and manifest presence set -e rg -nP '"auth"\s*:' pkg/datafetcher/schema || true rg -nP '"providers"|\"identities\"' pkg/datafetcher/schema || true fd -a 'schemas/atmos/atmos-manifest/1.0/atmos-manifest.json' || trueinternal/auth/docs/ARCHITECTURE.md (1)
26-29: Fix incorrect AWS file-management paths in Package Structure.These entries still reference environment/aws_files.go, but the implementation lives under cloud/aws (e.g., cloud/aws/files.go or cloud/aws/aws_files.go). Update the tree to match the codebase.
Run to confirm the actual paths:
#!/bin/bash fd -a 'internal/auth/(environment/aws_files.go|cloud/aws/*files.go|cloud/aws/*provider.go)'Apply:
-├── environment/ # Environment management -│ ├── aws_files.go # AWS credentials/config file management -│ └── merger.go # Environment variable merging +├── cloud/ # Cloud provider implementations +│ └── aws/ +│ ├── files.go # AWS credentials/config file management +│ └── provider.go # AWS cloud-provider adapterinternal/auth/docs/USER_GUIDE.md (1)
74-75: Annotate GitHub OIDC status.Per implementation and factory registration, mark as Stable.
-- **GitHub OIDC**: `github/oidc` +- **GitHub OIDC**: `github/oidc` — Status: Stableinternal/auth/validation/validator.go (5)
34-41: Compute provider/identity pointer maps once; avoid repeated conversions.Minor perf/readability improvement; reuse converted maps.
- // Validate providers - //nolint:gocritic // rangeValCopy: map stores structs; address of map element can't be taken. Passing copy to factory is intended. - for name, provider := range config.Providers { + // Precompute pointer maps for reuse. + providersPtr := convertProviders(config.Providers) + identitiesPtr := convertIdentities(config.Identities) + + // Validate providers. + //nolint:gocritic // rangeValCopy: map stores structs; address of map element can't be taken. + for name, provider := range config.Providers { if err := v.ValidateProvider(name, &provider); err != nil { return fmt.Errorf("%w: provider %q validation failed: %v", errUtils.ErrInvalidAuthConfig, name, err) } } - // Validate identities - - for name, identity := range config.Identities { - if err := v.ValidateIdentity(name, &identity, convertProviders(config.Providers)); err != nil { + // Validate identities. + for name, identity := range config.Identities { + if err := v.ValidateIdentity(name, &identity, providersPtr); err != nil { return fmt.Errorf("%w: identity %q validation failed: %v", errUtils.ErrInvalidAuthConfig, name, err) } } - // Validate chains - if err := v.ValidateChains(convertIdentities(config.Identities), convertProviders(config.Providers)); err != nil { + // Validate chains. + if err := v.ValidateChains(identitiesPtr, providersPtr); err != nil { return fmt.Errorf("%w: identity chain validation failed: %v", errUtils.ErrInvalidAuthConfig, err) }Also applies to: 44-53
109-116: Require via for non-user identities and enforce exactly one of provider/identity.Prevents misconfigured chains and runtime failures.
- // Validate via configuration - AWS User identities don't require via provider - if identity.Kind != "aws/user" && identity.Via != nil { - if identity.Via.Provider != "" { - if _, exists := providers[identity.Via.Provider]; !exists { - return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider) - } - } - } + // Validate via configuration — required for all non-user identities. + if identity.Kind != "aws/user" { + if identity.Via == nil { + return fmt.Errorf("%w: identity %q requires a via configuration", errUtils.ErrInvalidIdentityConfig, name) + } + // Exactly one of provider or identity must be set. + if (identity.Via.Provider == "" && identity.Via.Identity == "") || (identity.Via.Provider != "" && identity.Via.Identity != "") { + return fmt.Errorf("%w: identity %q must specify either via.provider or via.identity", errUtils.ErrInvalidIdentityConfig, name) + } + if identity.Via.Provider != "" { + if _, exists := providers[identity.Via.Provider]; !exists { + return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidIdentityConfig, identity.Via.Provider) + } + } + }
137-143: Validate referenced identities exist during graph build.Reject dangling references early with actionable error.
if identity.Via != nil { if identity.Via.Identity != "" { + if _, ok := identities[identity.Via.Identity]; !ok { + return fmt.Errorf("%w: referenced identity %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Identity) + } graph[name] = append(graph[name], identity.Via.Identity) } }
160-170: Use provider-scoped sentinel and tighten URL validation.Return ErrInvalidProviderConfig in provider validators; ensure SAML URL has scheme+host.
func (v *validator) validateSSOProvider(provider *schema.Provider) error { if provider.StartURL == "" { - return fmt.Errorf("%w: start_url is required for AWS SSO provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: start_url is required for AWS SSO provider", errUtils.ErrInvalidProviderConfig) } if provider.Region == "" { - return fmt.Errorf("%w: region is required for AWS SSO provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: region is required for AWS SSO provider", errUtils.ErrInvalidProviderConfig) } return nil } @@ func (v *validator) validateSAMLProvider(provider *schema.Provider) error { if provider.URL == "" { - return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidProviderConfig) } if provider.Region == "" { - return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidProviderConfig) } - // Validate URL format - if _, err := url.Parse(provider.URL); err != nil { - return fmt.Errorf("%w: invalid URL format: %v", errUtils.ErrInvalidAuthConfig, err) + // Validate URL format strictly (scheme + host required). + u, err := url.ParseRequestURI(provider.URL) + if err != nil || u.Scheme == "" || u.Host == "" { + return fmt.Errorf("%w: invalid URL format %q", errUtils.ErrInvalidProviderConfig, provider.URL) } return nil } @@ func (v *validator) validateGitHubOIDCProvider(provider *schema.Provider) error { if provider.Region == "" { - return fmt.Errorf("%w: region is required for GitHub OIDC provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: region is required for GitHub OIDC provider", errUtils.ErrInvalidProviderConfig) } return nil }Also applies to: 176-191, 194-199
237-239: Accept all AWS partitions for role ARN validation.Don’t hardcode arn:aws; permit aws-cn, aws-us-gov, etc.
- if !strings.HasPrefix(roleArn, "arn:aws:iam::") { - return fmt.Errorf("%w: invalid role ARN format", errUtils.ErrInvalidAuthConfig) - } + if !strings.HasPrefix(roleArn, "arn:aws") || !strings.Contains(roleArn, ":iam::") || !strings.Contains(roleArn, ":role/") { + return fmt.Errorf("%w: invalid role ARN format %q", errUtils.ErrInvalidIdentityConfig, roleArn) + }internal/auth/hooks.go (7)
25-31: Guard against nil inputs instead of silently skipping.Returning nil when stackInfo/atmosConfig are nil can mask misconfigurations. Fail fast with a wrapped sentinel.
func TerraformPreHook(atmosConfig *schema.AtmosConfiguration, stackInfo *schema.ConfigAndStacksInfo) error { - if !validateAuthConfig(atmosConfig, stackInfo) { + if stackInfo == nil { + return fmt.Errorf("%w: stackInfo is required", errUtils.ErrInvalidAuthConfig) + } + if atmosConfig == nil { + return fmt.Errorf("%w: atmosConfig is required", errUtils.ErrInvalidAuthConfig) + } + if !validateAuthConfig(atmosConfig, stackInfo) { log.Debug("No auth configuration found, skipping authentication") return nil }
54-58: Validate the auth manager before use.Catch misconfigurations early.
authManager, err := newAuthManager(&authConfig, stackInfo) if err != nil { return err } + + // Validate merged configuration before authenticating. + if err := authManager.Validate(); err != nil { + return fmt.Errorf("%w: invalid auth configuration: %w", errUtils.ErrInvalidAuthConfig, err) + }
67-71: Wrap default-identity lookup error with %w.Preserve the cause per Go idioms and your guidelines.
- if err != nil { - return fmt.Errorf("%w: failed to get default identity: %v", errUtils.ErrDefaultIdentity, err) - } + if err != nil { + return fmt.Errorf("%w: failed to get default identity: %w", errUtils.ErrDefaultIdentity, err) + }
79-82: Preserve authentication failure cause with %w.Keeps the error chain intact for troubleshooting.
- whoami, err := authManager.Authenticate(ctx, targetIdentityName) - if err != nil { - return fmt.Errorf("%w: failed to authenticate with identity %q: %v", errUtils.ErrAuthenticationFailed, targetIdentityName, err) - } + whoami, err := authManager.Authenticate(ctx, targetIdentityName) + if err != nil { + return fmt.Errorf("%w: failed to authenticate with identity %q: %w", errUtils.ErrAuthenticationFailed, targetIdentityName, err) + }
86-87: Propagate output errors and ensure YAML is written to stdout.Don’t drop the error; and the util currently only logs. Update both call-site and util.
- _ = utils.PrintAsYAMLToFileDescriptor(atmosConfig, stackInfo.ComponentEnvSection) - return nil + if err := utils.PrintAsYAMLToFileDescriptor(atmosConfig, stackInfo.ComponentEnvSection); err != nil { + return fmt.Errorf("%w: failed to emit environment configuration", err) + } + return nilAlso update pkg/utils/yaml_utils.go:
--- a/pkg/utils/yaml_utils.go +++ b/pkg/utils/yaml_utils.go @@ +import ( + "fmt" + "os" +) @@ - log.Debug("PrintAsYAMLToFileDescriptor", "data", y) - return nil + // Data/results must go to stdout for piping. + if _, err := fmt.Fprint(os.Stdout, y); err != nil { + return fmt.Errorf("write to stdout failed: %w", err) + } + return nil
121-139: Nil-safe log level parsing with sane defaults.Avoid NPEs when Logs/Auth.Logs are nil; default to current/info; only set parsed levels when valid.
-func getConfigLogLevels(atmosConfig *schema.AtmosConfiguration) (log.Level, log.Level) { - if atmosConfig == nil { - return log.InfoLevel, log.InfoLevel - } - atmosLevel := log.GetLevel() - if atmosConfig.Logs.Level != "" { - if l, err := log.ParseLevel(atmosConfig.Logs.Level); err == nil { - atmosLevel = l - } - } - // Determine auth log level (fallback to atmos level). - authLevel := atmosLevel - if atmosConfig.Auth.Logs.Level != "" { - if l, err := log.ParseLevel(atmosConfig.Auth.Logs.Level); err == nil { - authLevel = l - } - } - return atmosLevel, authLevel -} +func getConfigLogLevels(atmosConfig *schema.AtmosConfiguration) (log.Level, log.Level) { + // Base on current level; fall back to info when config is nil. + current := log.GetLevel() + if atmosConfig == nil { + return current, current + } + atmosLevel := current + if atmosConfig.Logs != nil && atmosConfig.Logs.Level != "" { + if l, err := log.ParseLevel(atmosConfig.Logs.Level); err == nil { + atmosLevel = l + } + } + authLevel := atmosLevel + if atmosConfig.Auth.Logs != nil && atmosConfig.Auth.Logs.Level != "" { + if l, err := log.ParseLevel(atmosConfig.Auth.Logs.Level); err == nil { + authLevel = l + } + } + return atmosLevel, authLevel +}
43-46: Preserve the original decode error with %w.Use %w for the cause to keep the error chain.
- err := mapstructure.Decode(stackInfo.ComponentAuthSection, &authConfig) - if err != nil { - return fmt.Errorf("%w: failed to decode component auth config: %v", errUtils.ErrInvalidAuthConfig, err) - } + err := mapstructure.Decode(stackInfo.ComponentAuthSection, &authConfig) + if err != nil { + return fmt.Errorf("%w: failed to decode component auth config: %w", errUtils.ErrInvalidAuthConfig, err) + }
🧹 Nitpick comments (7)
tests/fixtures/scenarios/auth/atmos.yaml (2)
14-14: Strip trailing whitespace (yamllint errors).- + - + - + - +Also applies to: 25-25, 34-34, 41-41
11-13: Clarify region precedence; avoid duplicate sources of truth.If provider sets region, omit it from user.credentials unless schema mandates it.
- region: us-east-1 + # region inherited from provider unless schema requires override + # region: us-east-1Also applies to: 36-41
internal/auth/docs/ARCHITECTURE.md (1)
9-9: Add fenced code languages to satisfy markdownlint (MD040).Declare languages for the directory tree and the flow diagram.
-``` +```text @@ -``` +```text Component Request → Identity → Provider Chain → Root ProviderAlso applies to: 81-84 </blockquote></details> <details> <summary>internal/auth/docs/USER_GUIDE.md (2)</summary><blockquote> `33-36`: **Clarify provider region requirements.** Region is also required for AWS SSO and AWS SAML per validator. ```diff Notes: -- Region is required for the GitHub OIDC provider and is validated at construction time. +- Region is required for GitHub OIDC, AWS SSO, and AWS SAML providers and is validated during construction.
396-401: Use proper headings instead of bold labels (MD036).Convert emphasized lines to headings.
-**Configuration Validation Errors** +#### Configuration Validation Errors @@ -**Authentication Failures** +#### Authentication Failures @@ -**Permission Errors** +#### Permission Errors @@ -**Environment Variable Issues** +#### Environment Variable IssuesAlso applies to: 402-414, 415-424, 425-434
internal/auth/hooks.go (2)
38-42: Fix godot lints: end comments with periods.golangci-lint (godot) will fail on these.
-// TODO: verify if we need to use Decode, or if we can use the merged auth config directly -// Use the merged auth configuration from stackInfo -// ComponentAuthSection already contains the deep-merged auth config from component + inherits + atmos.yaml -// Converted to typed struct when needed +// TODO: verify if we need to use Decode, or if we can use the merged auth config directly. +// Use the merged auth configuration from stackInfo. +// ComponentAuthSection already contains the deep-merged auth config from component + inherits + atmos.yaml. +// Converted to typed struct when needed. @@ -// Skip if no auth config (check the merged config, not the original) +// Skip if no auth config (check the merged config, not the original). @@ -// Determine target identity: stack info identity (CLI flag) or default identity +// Determine target identity: stack info identity (CLI flag) or default identity. @@ -// This is set by the CLI Flag +// This is set by the CLI flag. @@ -// Create auth manager components +// Create auth manager components. @@ -// Create auth manager with merged configuration and stack info (so identities can mutate it) +// Create auth manager with merged configuration and stack info (so identities can mutate it).Also applies to: 48-48, 59-59, 63-63, 104-104, 108-108
78-80: Optional: Add a timeout to authentication.Prevents hanging in CI/automation.
- ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel()Add:
import "time"
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
internal/auth/docs/ADDING_PROVIDERS.md(1 hunks)internal/auth/docs/ARCHITECTURE.md(1 hunks)internal/auth/docs/USER_GUIDE.md(1 hunks)internal/auth/factory.go(1 hunks)internal/auth/hooks.go(1 hunks)internal/auth/validation/validator.go(1 hunks)tests/fixtures/scenarios/auth/atmos.yaml(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/auth/factory.go
- internal/auth/docs/ADDING_PROVIDERS.md
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)
**/*.go: Use Viper for managing configuration, environment variables, and flags
Use interfaces for external dependencies to facilitate mocking
All code must pass golangci-lint checks
Follow Go's error handling idioms
Use meaningful error messages
Wrap errors with context using fmt.Errorf("context: %w", err)
Consider using a custom error type for domain-specific errors
Follow standard Go coding style
Use gofmt and goimports to format code
Prefer short, descriptive variable names
Use snake_case for environment variables
Document all exported functions, types, and methods
Document complex logic with inline comments
Follow Go's documentation conventions
Use Viper for configuration management
Support configuration via files, environment variables, and flags
Follow the precedence order: flags > environment variables > config file > defaults
**/*.go: All comments in Go code must end with periods (enforced by golangci-lint godot).
Wrap all returned errors using static errors from the errors package; never return dynamic errors directly.
Always bind environment variables with viper.BindEnv() and provide ATMOS_ alternatives for each external var.
Separate structured logging from UI output: use stderr for prompts/errors to user; stdout only for data; never use logging for UI.
Most text UI must go to stderr; only data/results to stdout. Prefer utils.PrintfMessageToTUI for UI messages.
For non-standard execution paths, capture telemetry with telemetry.CaptureCmd or telemetry.CaptureCmdString and never capture user data.
Ensure cross-platform compatibility: prefer SDKs to external binaries, use filepath/os/runtime for portability, and add build constraints when needed.
Files:
internal/auth/validation/validator.gointernal/auth/hooks.go
**/*.{yaml,yml}
📄 CodeRabbit inference engine (CLAUDE.md)
YAML configs should support Go templating using provided template functions.
Files:
tests/fixtures/scenarios/auth/atmos.yaml
🧬 Code graph analysis (2)
internal/auth/validation/validator.go (3)
internal/auth/types/interfaces.go (3)
Validator(157-169)Provider(53-72)Identity(75-95)pkg/schema/schema.go (5)
AuthConfig(980-984)Logs(366-369)LogsConfig(975-977)Provider(987-999)Identity(1007-1015)errors/errors.go (4)
ErrInvalidAuthConfig(83-83)ErrInvalidProviderConfig(87-87)ErrInvalidProviderKind(86-86)ErrInvalidIdentityConfig(85-85)
internal/auth/hooks.go (7)
internal/auth/types/interfaces.go (5)
Provider(53-72)Identity(75-95)AuthManager(98-136)CredentialStore(139-154)Validator(157-169)pkg/schema/schema.go (6)
Provider(987-999)Identity(1007-1015)AtmosConfiguration(25-61)ConfigAndStacksInfo(460-539)AuthConfig(980-984)Logs(366-369)errors/errors.go (4)
ErrInvalidAuthConfig(83-83)ErrDefaultIdentity(91-91)ErrAuthenticationFailed(88-88)ErrAuthManager(90-90)pkg/utils/yaml_utils.go (1)
PrintAsYAMLToFileDescriptor(102-115)internal/auth/credentials/store.go (1)
NewCredentialStore(29-31)internal/auth/validation/validator.go (1)
NewValidator(17-19)internal/auth/manager.go (1)
NewAuthManager(36-66)
🪛 markdownlint-cli2 (0.17.2)
internal/auth/docs/ARCHITECTURE.md
9-9: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
81-81: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
internal/auth/docs/USER_GUIDE.md
396-396: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
402-402: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
415-415: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
425-425: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
🪛 Checkov (3.2.334)
tests/fixtures/scenarios/auth/atmos.yaml
[high] 38-39: AWS Access Key
(CKV_SECRET_2)
🪛 YAMLlint (1.37.1)
tests/fixtures/scenarios/auth/atmos.yaml
[error] 14-14: trailing spaces
(trailing-spaces)
[error] 25-25: trailing spaces
(trailing-spaces)
[error] 34-34: trailing spaces
(trailing-spaces)
[error] 41-41: trailing spaces
(trailing-spaces)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Build (windows-latest, windows)
- GitHub Check: Build (ubuntu-latest, linux)
- GitHub Check: Analyze (go)
- GitHub Check: Lint (golangci)
- GitHub Check: Summary
🔇 Additional comments (2)
tests/fixtures/scenarios/auth/atmos.yaml (2)
22-24: Confirm account key naming matches schema (name vs id).If schema expects account.id (common), rename accordingly.
- account: - name: "123456789012" + account: + id: "123456789012"Also applies to: 31-33
42-49: Verify identity chaining for assume-role.If STS creds must originate from a concrete identity (e.g., permission-set), prefer via.identity: test-admin instead of via.provider.
test-role: kind: aws/assume-role via: - provider: test-sso + identity: test-admin
| func newAuthManager(authConfig *schema.AuthConfig, stackInfo *schema.ConfigAndStacksInfo) (types.AuthManager, error) { | ||
| // Create auth manager components | ||
| credStore := credentials.NewCredentialStore() | ||
| validator := validation.NewValidator() | ||
|
|
||
| // Create auth manager with merged configuration and stack info (so identities can mutate it) | ||
| authManager, err := NewAuthManager( | ||
| authConfig, | ||
| credStore, | ||
| validator, | ||
| stackInfo, | ||
| ) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("%w: failed to create auth manager: %v", errUtils.ErrAuthManager, err) | ||
| } | ||
| return authManager, nil | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Wrap NewAuthManager error with %w and richer context.
Use %w to retain the underlying cause.
authManager, err := NewAuthManager(
authConfig,
credStore,
validator,
stackInfo,
)
if err != nil {
- return nil, fmt.Errorf("%w: failed to create auth manager: %v", errUtils.ErrAuthManager, err)
+ return nil, fmt.Errorf("%w: failed to create auth manager: %w", errUtils.ErrAuthManager, err)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func newAuthManager(authConfig *schema.AuthConfig, stackInfo *schema.ConfigAndStacksInfo) (types.AuthManager, error) { | |
| // Create auth manager components | |
| credStore := credentials.NewCredentialStore() | |
| validator := validation.NewValidator() | |
| // Create auth manager with merged configuration and stack info (so identities can mutate it) | |
| authManager, err := NewAuthManager( | |
| authConfig, | |
| credStore, | |
| validator, | |
| stackInfo, | |
| ) | |
| if err != nil { | |
| return nil, fmt.Errorf("%w: failed to create auth manager: %v", errUtils.ErrAuthManager, err) | |
| } | |
| return authManager, nil | |
| } | |
| func newAuthManager(authConfig *schema.AuthConfig, stackInfo *schema.ConfigAndStacksInfo) (types.AuthManager, error) { | |
| // Create auth manager components | |
| credStore := credentials.NewCredentialStore() | |
| validator := validation.NewValidator() | |
| // Create auth manager with merged configuration and stack info (so identities can mutate it) | |
| authManager, err := NewAuthManager( | |
| authConfig, | |
| credStore, | |
| validator, | |
| stackInfo, | |
| ) | |
| if err != nil { | |
| return nil, fmt.Errorf("%w: failed to create auth manager: %w", errUtils.ErrAuthManager, err) | |
| } | |
| return authManager, nil | |
| } |
🤖 Prompt for AI Agents
In internal/auth/hooks.go around lines 103 to 119, the error formatting
currently uses fmt.Errorf("%w: failed to create auth manager: %v",
errUtils.ErrAuthManager, err) which does not wrap the underlying error; change
the return to wrap the actual creation error using %w (for example: return nil,
fmt.Errorf("failed to create auth manager: %w", err)) so the original cause is
preserved and surfaced to callers.
| return fmt.Errorf("%w: logs configuration validation failed: %w", errUtils.ErrInvalidAuthConfig, err) | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Fix multiple %w in fmt.Errorf (fails go vet/golangci-lint).
fmt.Errorf supports only one %w. Replace secondary %w with %v.
- return fmt.Errorf("%w: logs configuration validation failed: %w", errUtils.ErrInvalidAuthConfig, err)
+ return fmt.Errorf("%w: logs configuration validation failed: %v", errUtils.ErrInvalidAuthConfig, err)
@@
- return fmt.Errorf("%w: provider %q validation failed: %w", errUtils.ErrInvalidAuthConfig, name, err)
+ return fmt.Errorf("%w: provider %q validation failed: %v", errUtils.ErrInvalidAuthConfig, name, err)
@@
- return fmt.Errorf("%w: identity %q validation failed: %w", errUtils.ErrInvalidAuthConfig, name, err)
+ return fmt.Errorf("%w: identity %q validation failed: %v", errUtils.ErrInvalidAuthConfig, name, err)
@@
- return fmt.Errorf("%w: identity chain validation failed: %w", errUtils.ErrInvalidAuthConfig, err)
+ return fmt.Errorf("%w: identity chain validation failed: %v", errUtils.ErrInvalidAuthConfig, err)
@@
- return fmt.Errorf("%w: invalid URL format: %w", errUtils.ErrInvalidAuthConfig, err)
+ return fmt.Errorf("%w: invalid URL format: %v", errUtils.ErrInvalidAuthConfig, err)Also applies to: 38-39, 46-47, 52-53, 186-187
🏁 Script executed:
#!/bin/bash
rg -nP --type=go 'fmt\.Errorf\([^)]*%w[^)]*%w' internal | sed -n '1,120p'Length of output: 1992
Fix multiple %w in fmt.Errorf across the codebase
Use only one %w per fmt.Errorf call and replace any secondary %w with %v:
- internal/auth/validation/validator.go: lines 30, 38, 46, 52, 187
- internal/auth/providers/github/oidc.go: lines 100, 124, 136, 146
- internal/auth/manager.go: line 509
- internal/auth/identities/aws/user.go: line 79
- internal/auth/cloud/aws/files.go: line 49
- internal/exec/yaml_func_store_get.go: line 98
- internal/exec/terraform_generate_planfile.go: line 219
🤖 Prompt for AI Agents
In internal/auth/validation/validator.go around lines 30-52 and 187, multiple
fmt.Errorf calls use more than one %w; update each fmt.Errorf so it uses exactly
one %w (for the error you intend to wrap) and replace any additional %w
specifiers with %v (or %s) to print other error details or values, preserving
the original message order and semantics; run go vet to confirm only a single %w
remains per call.
There was a problem hiding this comment.
golangci-lint found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
tests/fixtures/scenarios/auth/atmos.yaml (2)
65-71: Verify schemas exist and model new auth fields.The manifest path and schema defs for auth.providers/identities must exist and align with this fixture. Please verify and add if missing.
#!/bin/bash set -euo pipefail echo "== Check atmos manifest path ==" fd -a 'schemas/atmos/atmos-manifest/1.0/atmos-manifest.json' || true echo "== Check schema contains auth.providers & auth.identities ==" rg -nP '"auth"\s*:' pkg/datafetcher/schema || true rg -nP '"providers"|\"identities\"' pkg/datafetcher/schema || true
35-41: Good: test credentials are no longer AWS-shaped.This avoids tripping secret scanners.
🧹 Nitpick comments (6)
internal/auth/manager.go (6)
395-401: Don’t ignore provider.PreAuthenticate errors; log and continue.Capture the error to aid troubleshooting without failing the flow.
- if provider, exists := m.providers[m.chain[0]]; exists { - _ = provider.PreAuthenticate(m) - } + if provider, exists := m.providers[m.chain[0]]; exists { + if err := provider.PreAuthenticate(m); err != nil { + log.Warn("PreAuthenticate failed", "provider", m.chain[0], "error", err) + } + }
606-613: Provider resolution for Whoami: ensure root provider even when chain not set.Fallback to chain[0] or rebuild the chain to avoid mislabeling for nested identities.
-func (m *manager) buildWhoamiInfo(identityName string, creds types.ICredentials) *types.WhoamiInfo { - providerName := m.getProviderForIdentity(identityName) +func (m *manager) buildWhoamiInfo(identityName string, creds types.ICredentials) *types.WhoamiInfo { + providerName := m.getProviderForIdentity(identityName) + if len(m.chain) > 0 && m.chain[0] != "" { + providerName = m.chain[0] + } else if chain, err := m.buildAuthenticationChain(identityName); err == nil && len(chain) > 0 { + providerName = chain[0] + }
620-625: Merge provider env vars into WhoamiInfo.Currently only identity env is included; add provider env for completeness.
- // Get environment variables + // Get environment variables (provider + identity). + if providerName != "" { + if p, ok := m.providers[providerName]; ok { + if penv, err := p.Environment(); err == nil && len(penv) > 0 { + if info.Environment == nil { + info.Environment = map[string]string{} + } + for k, v := range penv { + info.Environment[k] = v + } + } + } + } if identity, exists := m.identities[identityName]; exists { if env, err := identity.Environment(); err == nil { info.Environment = env } }
442-451: Guard against out-of-range startIndex in retrieveCachedCredentials.Defensive check prevents potential panic on future refactors.
func (m *manager) retrieveCachedCredentials(chain []string, startIndex int) (types.ICredentials, error) { + if startIndex < 0 || startIndex >= len(chain) { + return nil, fmt.Errorf("%w: invalid startIndex %d", errUtils.ErrInvalidAuthConfig, startIndex) + } identityName := chain[startIndex] currentCreds, err := m.credentialStore.Retrieve(identityName) if err != nil { return nil, err }
22-23: Doc comment polish (typo + periods for godot).Fix casing and end sentences with periods.
-// manager implements the Authmanager interface. +// manager implements the AuthManager interface. @@ -// NewAuthManager creates a new Authmanager instance. +// NewAuthManager creates a new AuthManager instance. @@ -// getProviderForIdentity returns the provider name for the given identity +// getProviderForIdentity returns the provider name for the given identity. @@ -// GetProviderForIdentity returns the provider name for the given identity +// GetProviderForIdentity returns the provider name for the given identity. @@ -// GetProviderKindForIdentity returns the provider kind for the given identity +// GetProviderKindForIdentity returns the provider kind for the given identity. @@ -// buildAuthenticationChain builds the authentication chain from target identity to source provider -// Returns a slice where [0] is the provider name, [1..n] are identity names in authentication order +// buildAuthenticationChain builds the authentication chain from target identity to source provider. +// Returns a slice where [0] is the provider name, [1..n] are identity names in authentication order. @@ -// GetConfig returns the config. +// GetConfig returns the current stack info pointer.Also applies to: 35-36, 238-240, 266-268, 272-274, 531-533, 429-433
488-491: Optional: standardize log key names (snake_case).E.g., chain_length/start_index/provider to match docs.
Also applies to: 89-90, 450-472
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
internal/auth/hooks.go(1 hunks)internal/auth/manager.go(1 hunks)internal/exec/utils_test.go(3 hunks)tests/fixtures/scenarios/auth/atmos.yaml(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/auth/hooks.go
- internal/exec/utils_test.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)
**/*.go: Use Viper for managing configuration, environment variables, and flags
Use interfaces for external dependencies to facilitate mocking
All code must pass golangci-lint checks
Follow Go's error handling idioms
Use meaningful error messages
Wrap errors with context using fmt.Errorf("context: %w", err)
Consider using a custom error type for domain-specific errors
Follow standard Go coding style
Use gofmt and goimports to format code
Prefer short, descriptive variable names
Use snake_case for environment variables
Document all exported functions, types, and methods
Document complex logic with inline comments
Follow Go's documentation conventions
Use Viper for configuration management
Support configuration via files, environment variables, and flags
Follow the precedence order: flags > environment variables > config file > defaults
**/*.go: All comments in Go code must end with periods (enforced by golangci-lint godot).
Wrap all returned errors using static errors from the errors package; never return dynamic errors directly.
Always bind environment variables with viper.BindEnv() and provide ATMOS_ alternatives for each external var.
Separate structured logging from UI output: use stderr for prompts/errors to user; stdout only for data; never use logging for UI.
Most text UI must go to stderr; only data/results to stdout. Prefer utils.PrintfMessageToTUI for UI messages.
For non-standard execution paths, capture telemetry with telemetry.CaptureCmd or telemetry.CaptureCmdString and never capture user data.
Ensure cross-platform compatibility: prefer SDKs to external binaries, use filepath/os/runtime for portability, and add build constraints when needed.
Files:
internal/auth/manager.go
**/*.{yaml,yml}
📄 CodeRabbit inference engine (CLAUDE.md)
YAML configs should support Go templating using provided template functions.
Files:
tests/fixtures/scenarios/auth/atmos.yaml
🧠 Learnings (1)
📚 Learning: 2025-09-08T16:26:41.558Z
Learnt from: Benbentwo
PR: cloudposse/atmos#1452
File: internal/auth/manager.go:0-0
Timestamp: 2025-09-08T16:26:41.558Z
Learning: charmbracelet/log library supports printf-style logging methods including Debugf, Infof, Warnf, Errorf, and Fatalf in addition to structured logging methods.
Applied to files:
internal/auth/manager.go
🧬 Code graph analysis (1)
internal/auth/manager.go (7)
pkg/schema/schema.go (7)
AuthConfig(980-984)Provider(987-999)Identity(1007-1015)ConfigAndStacksInfo(460-539)Context(371-386)Validate(184-186)Alias(91-91)internal/auth/types/interfaces.go (6)
Provider(53-72)Identity(75-95)CredentialStore(139-154)Validator(157-169)AuthManager(98-136)ICredentials(171-177)errors/errors.go (5)
ErrInvalidAuthConfig(83-83)ErrAuthenticationFailed(88-88)ErrUnsupportedInputType(35-35)ErrInvalidProviderConfig(87-87)ErrInvalidIdentityConfig(85-85)internal/auth/types/structs.go (1)
WhoamiInfo(69-79)pkg/telemetry/ci.go (1)
IsCI(72-74)internal/auth/factory.go (2)
NewProvider(15-29)NewIdentity(32-46)internal/auth/identities/aws/user.go (2)
IsStandaloneAWSUserChain(257-268)AuthenticateStandaloneAWSUser(271-288)
🪛 YAMLlint (1.37.1)
tests/fixtures/scenarios/auth/atmos.yaml
[error] 14-14: trailing spaces
(trailing-spaces)
[error] 25-25: trailing spaces
(trailing-spaces)
[error] 34-34: trailing spaces
(trailing-spaces)
[error] 41-41: trailing spaces
(trailing-spaces)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Build (windows-latest, windows)
- GitHub Check: Build (ubuntu-latest, linux)
- GitHub Check: Analyze (go)
- GitHub Check: Lint (golangci)
- GitHub Check: Summary
🔇 Additional comments (1)
tests/fixtures/scenarios/auth/atmos.yaml (1)
35-41: Verify region placement for aws/user –credentialsis an untyped map, so the schema won’t enforce whereregionlives; confirm your aws/user implementation actually readscredentials.region(rather than a top-levelregionon the provider)
| "github.com/charmbracelet/huh" | ||
| "github.com/charmbracelet/lipgloss" | ||
| log "github.com/charmbracelet/log" |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Drop lipgloss styling from logs; use structured logs only.
Avoid formatting/ANSI styling in logs per Atmos structured-logging guidelines. Remove lipgloss import and bold-rendered values.
import (
"context"
"fmt"
"time"
- "github.com/charmbracelet/lipgloss"
log "github.com/charmbracelet/log"
@@
-func (m *manager) authenticateIdentityChain(ctx context.Context, startIndex int, initialCreds types.ICredentials) (types.ICredentials, error) {
- bold := lipgloss.NewStyle().Bold(true)
+func (m *manager) authenticateIdentityChain(ctx context.Context, startIndex int, initialCreds types.ICredentials) (types.ICredentials, error) {
@@
- if !exists {
- log.Error("Chaining identity failed",
- "from", bold.Render(m.getChainStepName(i-1)),
- "to", bold.Render(identityStep))
+ if !exists {
+ log.Error("Chaining identity failed",
+ "from", m.getChainStepName(i-1),
+ "to", identityStep)
return nil, fmt.Errorf("%w: identity %q not found in chain step %d", errUtils.ErrInvalidAuthConfig, identityStep, i)
}
@@
- if err != nil {
- log.Error("Chaining identity failed",
- "from", bold.Render(m.getChainStepName(i-1)),
- "to", bold.Render(identityStep))
+ if err != nil {
+ log.Error("Chaining identity failed",
+ "from", m.getChainStepName(i-1),
+ "to", identityStep)
return nil, fmt.Errorf("%w: identity %q authentication failed at chain step %d: %w", errUtils.ErrAuthenticationFailed, identityStep, i, err)
}
@@
- log.Info("Chained identity", "from", bold.Render(m.getChainStepName(i-1)), "to", bold.Render(identityStep))
+ log.Info("Chained identity", "from", m.getChainStepName(i-1), "to", identityStep)
}Also applies to: 486-526
🤖 Prompt for AI Agents
In internal/auth/manager.go around lines 8 to 10 (and also review usages in
486-526), the code imports lipgloss and uses bold-styled strings in log
messages; per Atmos structured-logging guidelines remove the lipgloss import and
any lipgloss-based styling from logs. Replace styled/bold-rendered values with
plain strings or structured fields passed to the logger (e.g., log.Info/Debug
with key/value pairs) and remove the unused lipgloss import line; ensure all
occurrences in the specified ranges log raw values or use structured fields
instead of ANSI-styled text.
| kind: aws/iam-identity-center | ||
| region: us-east-1 | ||
| start_url: https://test.awsapps.com/start | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Fix YAML trailing whitespace (fails yamllint).
Remove the trailing spaces on these blank lines.
-
+
@@
-
+
@@
-
+
@@
-
+Also applies to: 25-25, 34-34, 41-41
🧰 Tools
🪛 YAMLlint (1.37.1)
[error] 14-14: trailing spaces
(trailing-spaces)
🤖 Prompt for AI Agents
In tests/fixtures/scenarios/auth/atmos.yaml around line 14 (and also lines 25,
34, 41), there are blank lines that contain trailing spaces which cause yamllint
failures; remove any trailing whitespace on those blank lines so they are truly
empty (no spaces or tabs), verify the file ends with a single newline, and
re-run yamllint to confirm the warnings are resolved.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
tests/test-cases/core.yaml (1)
81-81: Tighten the regex to be precise.The dot is a regex wildcard; anchor and escape to avoid accidental matches.
- - "Opening default browser to 'https://atmos.tools'." + - "^Opening default browser to 'https://atmos\\.tools'\\.$"tests/snapshots/TestCLICommands_atmos_auth_exec_--help.stderr.golden (2)
4-4: Fix glob text: '*.yaml' should be '*.yaml'The first path shows '//*.yaml' while the parenthetical shows '/*.yaml'. Make them consistent to avoid confusion and false negatives when matching snapshots.
-failed to load atmos config: failed to find a match for the import '/absolute/path/to/repo/tests/fixtures/scenarios/auth/stacks/**/*****.yaml' ('/absolute/path/to/repo/tests/fixtures/scenarios/auth/stacks' + '**/*.yaml') +failed to load atmos config: failed to find a match for the import '/absolute/path/to/repo/tests/fixtures/scenarios/auth/stacks/**/*.yaml' ('/absolute/path/to/repo/tests/fixtures/scenarios/auth/stacks' + '**/*.yaml')
2-6: Trim whitespace and standardize error prefixThere are trailing spaces and extra blank lines. Prefer a compact, deterministic message and use “Error:” (no Markdown header) for stderr.
- -# Error - -failed to load atmos config: failed to find a match for the import '/absolute/path/to/repo/tests/fixtures/scenarios/auth/stacks/**/*****.yaml' ('/absolute/path/to/repo/tests/fixtures/scenarios/auth/stacks' + '**/*.yaml') - - +Error: +failed to load atmos config: failed to find a match for the import '/absolute/path/to/repo/tests/fixtures/scenarios/auth/stacks/**/*.yaml' ('/absolute/path/to/repo/tests/fixtures/scenarios/auth/stacks' + '**/*.yaml')
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (16)
examples/demo-auth/atmos.yaml(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--help.stdout.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_exec_--help.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_login_--help.stdout.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_login_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_user_configure_--help.stdout.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_validate_(invalid_config).stdout.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_validate_--help.stdout.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_whoami_(CI_mode).stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_whoami_--help.stdout.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_whoami_--identity_nonexistent.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_whoami_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_docs.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_docs.stdout.golden(0 hunks)tests/snapshots/TestCLICommands_atmos_stacks_with_relative_paths.stdout.golden(3 hunks)tests/test-cases/core.yaml(1 hunks)
💤 Files with no reviewable changes (1)
- tests/snapshots/TestCLICommands_atmos_docs.stdout.golden
✅ Files skipped from review due to trivial changes (7)
- tests/snapshots/TestCLICommands_atmos_auth_validate_--help.stdout.golden
- tests/snapshots/TestCLICommands_atmos_auth_whoami_--help.stdout.golden
- tests/snapshots/TestCLICommands_atmos_auth_whoami_--identity_nonexistent.stderr.golden
- tests/snapshots/TestCLICommands_atmos_docs.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_whoami_(CI_mode).stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_validate_(invalid_config).stdout.golden
- tests/snapshots/TestCLICommands_atmos_auth_whoami_--identity_test-user.stderr.golden
🚧 Files skipped from review as they are similar to previous changes (5)
- tests/snapshots/TestCLICommands_atmos_auth_login_--identity_test-user.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_login_--help.stdout.golden
- tests/snapshots/TestCLICommands_atmos_auth_user_configure_--help.stdout.golden
- examples/demo-auth/atmos.yaml
- tests/snapshots/TestCLICommands_atmos_auth_env_--help.stdout.golden
🧰 Additional context used
📓 Path-based instructions (2)
tests/test-cases/**
📄 CodeRabbit inference engine (CLAUDE.md)
Never modify golden snapshot fixtures under tests/test-cases/ unless explicitly instructed.
Files:
tests/test-cases/core.yaml
**/*.{yaml,yml}
📄 CodeRabbit inference engine (CLAUDE.md)
YAML configs should support Go templating using provided template functions.
Files:
tests/test-cases/core.yaml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Build (ubuntu-latest, linux)
- GitHub Check: Build (windows-latest, windows)
- GitHub Check: Analyze (go)
- GitHub Check: Lint (golangci)
- GitHub Check: Summary
🔇 Additional comments (6)
tests/test-cases/core.yaml (2)
80-82: Good move: route UX text to stderr and keep stdout clean.This aligns with “stdout for data/results, stderr for human-readable messages.” The empty stdout assertion plus stderr match looks right.
67-82: Test harness already sets GO_TEST=1; browser opening disabled
tests/cli_test.go:153 and pkg/utils/url_utils.go confirm GO_TEST=1 is injected and used to bypass real browser launches.tests/snapshots/TestCLICommands_atmos_stacks_with_relative_paths.stdout.golden (3)
40-40: Prod snapshot update — OK.Matches dev; no issues.
71-71: Staging snapshot update — OK.Consistent with other environments.
9-9: All golden snapshots and consumers verified; only TestCLICommands_atmos_stacks_with_relative_paths.stdout.golden contains the new auth field, and no scripts consumeatmos stacksoutput—no further changes needed.tests/snapshots/TestCLICommands_atmos_auth_exec_--help.stderr.golden (1)
4-4: Snapshot path-agnosticism already implemented. ThesanitizeOutputfunction normalizes all paths to forward slashes and replaces the repository root with the fixed/absolute/path/to/repo/placeholder, satisfying the path-agnostic requirement.
| # Error | ||
|
|
||
| failed to load atmos config: failed to find a match for the import '/absolute/path/to/repo/tests/fixtures/scenarios/auth/stacks/**/*****.yaml' ('/absolute/path/to/repo/tests/fixtures/scenarios/auth/stacks' + '**/*.yaml') |
There was a problem hiding this comment.
--help should not initialize config or error
Help must print usage (stdout) and exit 0. Loading Atmos config here is a UX bug and makes the snapshot brittle. Move config init out of cobra.OnInitialize/PersistentPreRun(E) or guard it for help/completion paths.
Example guard:
func skipInit(cmd *cobra.Command) bool {
if cmd.Name() == "help" || cmd.HasSubCommands() && cmd.Flags().Changed("help") {
return true
}
if h := cmd.Flags().Lookup("help"); h != nil && h.Changed {
return true
}
return false
}
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if skipInit(cmd) { return nil }
return initConfig()
}🤖 Prompt for AI Agents
In tests/snapshots/TestCLICommands_atmos_auth_exec_--help.stderr.golden around
lines 2-4, the CLI help path is triggering Atmos config initialization which
causes an error and makes the --help snapshot brittle; adjust the command
initialization so help/completion/--help paths skip config loading. Remove or
guard the config init from cobra.OnInitialize/PersistentPreRun(E) and instead
add a skipInit check at the top of PersistentPreRunE (or wrap initConfig call)
that returns early when cmd.Name()== "help" or the help flag is present/changed
or when invoking completion/help subcommands; ensure help prints usage and
returns nil/exit 0 without loading config.
There was a problem hiding this comment.
Actionable comments posted: 9
♻️ Duplicate comments (8)
internal/auth/validation/validator.go (5)
31-31: Fix double %w (go vet).Each fmt.Errorf must contain at most one %w; see earlier diff for replacements to %v.
Also applies to: 39-39, 45-45, 182-182
127-138: Validate referenced identities exist when building the graph.Rejects dangling edges early.
for name, identity := range identities { if identity.Via != nil { if identity.Via.Identity != "" { + if _, ok := identities[identity.Via.Identity]; !ok { + return fmt.Errorf("%w: referenced identity %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Identity) + } graph[name] = append(graph[name], identity.Via.Identity) } } }
189-195: Use provider-scoped sentinel for GitHub OIDC errors.if provider.Region == "" { - return fmt.Errorf("%w: region is required for GitHub OIDC provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: region is required for GitHub OIDC provider", errUtils.ErrInvalidProviderConfig) }
155-166: Use provider-scoped sentinel for SSO provider errors.Aligns with error taxonomy.
if provider.StartURL == "" { - return fmt.Errorf("%w: start_url is required for AWS SSO provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: start_url is required for AWS SSO provider", errUtils.ErrInvalidProviderConfig) } if provider.Region == "" { - return fmt.Errorf("%w: region is required for AWS SSO provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: region is required for AWS SSO provider", errUtils.ErrInvalidProviderConfig) }
102-110: Require via for non-user identities and enforce exactly one of provider/identity.Prevents misconfiguration and later runtime errors.
-// Validate via configuration - AWS User identities don't require via provider -if identity.Kind != "aws/user" && identity.Via != nil { - if identity.Via.Provider != "" { - if _, exists := providers[identity.Via.Provider]; !exists { - return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider) - } - } -} +// Non-user identities must specify via, with exactly one of provider or identity. +if identity.Kind != "aws/user" { + if identity.Via == nil { + return fmt.Errorf("%w: identity %q requires a via configuration", errUtils.ErrInvalidIdentityConfig, name) + } + hasProv := identity.Via.Provider != "" + hasIdent := identity.Via.Identity != "" + if hasProv == hasIdent { + return fmt.Errorf("%w: identity %q must specify exactly one of via.provider or via.identity", errUtils.ErrInvalidIdentityConfig, name) + } + if hasProv { + if _, exists := providers[identity.Via.Provider]; !exists { + return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider) + } + } +}internal/auth/identities/aws/permission_set.go (1)
3-16: Use SSO access token correctly and discover the actual role name (with pagination).
- Don’t overload AWSCredentials.AccessKeyID with the SSO token; pass the token via AccessToken params and drop the fake credentials provider.
- Permission set name ≠ role name; use ListAccountRoles and paginate accounts/roles.
Apply:
@@ import ( "context" "fmt" "time" + "strings" awssdk "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sso" @@ - // Create AWS config using the base credentials (SSO access token) - cfg, err := config.LoadDefaultConfig(ctx, - config.WithRegion(awsBase.Region), - config.WithCredentialsProvider(awssdk.CredentialsProviderFunc(func(ctx context.Context) (awssdk.Credentials, error) { - return awssdk.Credentials{ - AccessKeyID: awsBase.AccessKeyID, // This is actually the SSO access token - }, nil - })), - ) + // Create AWS config using the base region. + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(awsBase.Region)) if err != nil { return nil, fmt.Errorf("%w: failed to load AWS config: %v", errUtils.ErrInvalidIdentityConfig, err) } @@ - // List accounts to find the target account ID - accountsResp, err := ssoClient.ListAccounts(ctx, &sso.ListAccountsInput{ - AccessToken: awssdk.String(awsBase.AccessKeyID), // SSO access token - }) - if err != nil { - return nil, fmt.Errorf("%w: failed to list accounts: %v", errUtils.ErrAwsAuth, err) - } - - if okAccountName { - for _, account := range accountsResp.AccountList { - if awssdk.ToString(account.AccountName) == accountName { - accountID = awssdk.ToString(account.AccountId) - break - } - } - if accountID == "" { - return nil, fmt.Errorf("%w: account %q not found", errUtils.ErrAwsAuth, accountName) - } - } + // Resolve account ID by name if needed (paginated). + if accountID == "" && accountName != "" { + var nextToken *string + for { + accountsResp, err := ssoClient.ListAccounts(ctx, &sso.ListAccountsInput{ + AccessToken: awssdk.String(awsBase.SessionToken), + NextToken: nextToken, + MaxResults: awssdk.Int32(1000), + }) + if err != nil { + return nil, fmt.Errorf("%w: failed to list accounts: %v", errUtils.ErrAwsAuth, err) + } + for _, account := range accountsResp.AccountList { + if awssdk.ToString(account.AccountName) == accountName { + accountID = awssdk.ToString(account.AccountId) + break + } + } + if accountID != "" || accountsResp.NextToken == nil { + break + } + nextToken = accountsResp.NextToken + } + if accountID == "" { + return nil, fmt.Errorf("%w: account %q not found", errUtils.ErrAwsAuth, accountName) + } + } - // Get role credentials for the permission set - roleCredsResp, err := ssoClient.GetRoleCredentials(ctx, &sso.GetRoleCredentialsInput{ - AccountId: awssdk.String(accountID), - RoleName: awssdk.String(permissionSetName), - AccessToken: awssdk.String(awsBase.AccessKeyID), - }) + // Discover the actual role name for the permission set (paginated). + var roleName string + var nextToken *string + for { + rolesResp, err := ssoClient.ListAccountRoles(ctx, &sso.ListAccountRolesInput{ + AccessToken: awssdk.String(awsBase.SessionToken), + AccountId: awssdk.String(accountID), + NextToken: nextToken, + MaxResults: awssdk.Int32(1000), + }) + if err != nil { + return nil, fmt.Errorf("%w: failed to list account roles: %v", errUtils.ErrAwsAuth, err) + } + for _, r := range rolesResp.RoleList { + rn := awssdk.ToString(r.RoleName) + if rn == permissionSetName || strings.HasPrefix(rn, "AWSReservedSSO_"+permissionSetName+"_") { + roleName = rn + break + } + } + if roleName != "" || rolesResp.NextToken == nil { + break + } + nextToken = rolesResp.NextToken + } + if roleName == "" { + return nil, fmt.Errorf("%w: no role found for permission set %q in account %q", errUtils.ErrAwsAuth, permissionSetName, accountName) + } + + // Get role credentials using the discovered role name. + roleCredsResp, err := ssoClient.GetRoleCredentials(ctx, &sso.GetRoleCredentialsInput{ + AccountId: awssdk.String(accountID), + RoleName: awssdk.String(roleName), + AccessToken: awssdk.String(awsBase.SessionToken), + }) if err != nil { return nil, fmt.Errorf("%w: failed to get role credentials: %v", errUtils.ErrAuthenticationFailed, err) }Note: Ensure the SSO provider populates AWSCredentials.SessionToken with the SSO access token; otherwise introduce a dedicated SSOCredentials/OIDCCredentials type.
Also applies to: 81-90, 94-125
pkg/schema/schema.go (2)
457-457: Wire the identity flag/env end-to-end (verify).You added Identity to ArgsAndFlagsInfo and ConfigAndStacksInfo; ensure a Cobra flag exists and viper.BindEnv("identity","ATMOS_IDENTITY") is set so precedence is flags > env > config.
Run:
#!/bin/bash rg -nP --type=go -C2 '\.(Persistent)?Flags\(\)\.(String|StringVar|StringP|StringVarP)\([^"]*"identity"' rg -nP --type=go 'viper\.BindEnv\(\s*"identity"\s*,\s*"ATMOS_IDENTITY"\s*\)' rg -nP --type=go '\bATMOS_IDENTITY\b'Also applies to: 538-538
974-979: Fix doc comments to pass golangci-lint (godot).Add trailing periods to exported-type comments.
Apply:
-// AuthConfig defines the authentication configuration structure +// AuthConfig defines the authentication configuration structure. @@ -// Provider defines an authentication provider configuration +// Provider defines an authentication provider configuration. @@ -// SessionConfig defines session configuration for providers +// SessionConfig defines session configuration for providers. @@ -// IdentityVia defines how an identity connects to a provider or other identity +// IdentityVia defines how an identity connects to a provider or other identity. @@ -// EnvironmentVariable defines an environment variable with preserved case sensitivity +// EnvironmentVariable defines an environment variable with preserved case sensitivity. @@ -// ComponentAuthConfig defines auth configuration at the component level +// ComponentAuthConfig defines auth configuration at the component level.Also applies to: 981-995, 996-1000, 1012-1017, 1018-1023, 1024-1029
🧹 Nitpick comments (17)
tests/fixtures/scenarios/atmos-auth/stacks/deploy/component-auth-configured.yaml (2)
15-18: Identity is missing kind; likely to fail schema/validation.Per schema.Identity, kind is expected. If omission is intentional for a merge-only fixture, disregard; otherwise add the concrete identity kind.
identities: identity-oidc: + kind: aws/permission_set # or aws/assume_role, as appropriate via: provider: provider-oidc
19-19: Fix trailing spaces and ensure newline at EOF.YAMLlint flags trailing spaces and missing final newline.
- +tests/fixtures/scenarios/atmos-auth-invalid/atmos.yaml (1)
13-13: Remove trailing whitespace.Keeps fixtures clean and avoids lint noise.
- +tests/fixtures/scenarios/atmos-auth/stacks/deploy/nonprod.yaml (1)
9-9: Add newline at EOF.Silences YAMLlint new-line-at-end-of-file.
- component: mock_caller_identity + component: mock_caller_identity +tests/test-cases/auth-merge.yaml (1)
21-26: Make assertion less brittle by matching structured output.Instead of matching a YAML snippet, consider asserting JSON keys/values (if available via -o json) to reduce whitespace/indent sensitivity.
tests/test-cases/auth-default-identity.yaml (2)
115-116: Align test name with args (admin-1).Prevents confusion when scanning CI logs.
- - name: atmos auth whoami --identity admin (multiple defaults) + - name: atmos auth whoami --identity admin-1 (multiple defaults)
3-148: Deduplicate common test fields with YAML anchors.DRY the repeated workdir/command/env to make edits cheaper.
Example:
tests: + - &defaults + workdir: "fixtures/scenarios/atmos-auth/" + command: "atmos" - name: atmos auth validate - workdir: "fixtures/scenarios/atmos-auth/" - command: "atmos" + <<: *defaults args: - "auth" - "validate"tests/snapshots/TestCLICommands_atmos_auth_env_--format_export_--identity_test-user.stderr.golden (1)
4-4: Snapshot includes absolute, OS-specific paths; consider normalizing.Normalize or redact repo paths in the produced error (or in test harness) to avoid cross-platform flakiness.
tests/test-cases/auth-cli.yaml (1)
3-337: Reduce duplication with YAML anchors for workdir/command and common env.Keeps the 18-case suite maintainable.
tests: + - &defaults + workdir: "fixtures/scenarios/atmos-auth/" + command: "atmos" - name: atmos auth validate - workdir: "fixtures/scenarios/atmos-auth/" - command: "atmos" + <<: *defaults args: - "auth" - "validate"internal/auth/types/whoami.go (1)
13-13: Consider masking or omitting sensitive env values in Whoami output.Environment can include AWS_SESSION_TOKEN, etc. Either exclude by default or mask values at the CLI layer.
tests/test-cases/auth-validation.yaml (2)
10-10: Trim trailing spaces to satisfy linters.Removes YAMLlint errors.
- +Also applies to: 16-16, 21-21, 39-39, 48-48, 55-55, 58-58
22-37: Route human-readable output to stderr per guidelines.“Validating authentication configuration” and the success line should go to stderr; stdout should be data-only. Update the command implementation and adjust test expectations accordingly.
Do you want me to open a follow-up to flip these streams and update snapshots?
internal/auth/types/github_oidc_credentials.go (1)
23-26: End inline comment with a period (godot).Satisfy lint rule requiring comments to end with periods.
func (c *OIDCCredentials) BuildWhoamiInfo(info *WhoamiInfo) { - // No additional fields to populate for generic OIDC creds + // No additional fields to populate for generic OIDC creds. }tests/fixtures/scenarios/atmos-auth/atmos.yaml (1)
10-10: Trim trailing spaces to keep fixtures lint-clean.- +Also applies to: 16-16, 21-21, 39-39, 48-48, 55-55, 58-58
internal/auth/validation/validator.go (2)
65-66: Message consistency: quote dynamic values and end sentences with periods.Optional polish; improves UX.
Also applies to: 88-89, 123-124
28-28: Nit: comment periods and section headers.Add trailing periods and unify comment style.
Also applies to: 35-37, 43-45
internal/auth/identities/aws/permission_set.go (1)
61-69: Add trailing periods to comments to satisfy godot.Several inline comments lack trailing periods (e.g., Lines 61, 68, 81, 94, 117, 127, 143). Please append periods.
Also applies to: 81-82, 94-97, 117-118, 127-128, 141-144
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (28)
internal/auth/hooks.go(1 hunks)internal/auth/identities/aws/permission_set.go(1 hunks)internal/auth/types/aws_credentials.go(1 hunks)internal/auth/types/github_oidc_credentials.go(1 hunks)internal/auth/types/whoami.go(1 hunks)internal/auth/validation/validator.go(1 hunks)pkg/schema/schema.go(5 hunks)tests/fixtures/scenarios/atmos-auth-invalid/atmos.yaml(1 hunks)tests/fixtures/scenarios/atmos-auth/atmos.yaml(1 hunks)tests/fixtures/scenarios/atmos-auth/stacks/deploy/component-auth-configured.yaml(1 hunks)tests/fixtures/scenarios/atmos-auth/stacks/deploy/nonprod.yaml(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_(CI_mode).stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_(multiple_defaults_-_CI).stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_(no_defaults_-_CI).stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--format_dotenv_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--format_export_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--format_invalid.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--format_json_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--identity_readonly_(no_defaults).stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_exec_--help.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_exec_--identity_test-user_--_echo_hello.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_login_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_merging.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_merging.stdout.golden(1 hunks)tests/test-cases/auth-cli.yaml(1 hunks)tests/test-cases/auth-default-identity.yaml(1 hunks)tests/test-cases/auth-merge.yaml(1 hunks)tests/test-cases/auth-validation.yaml(1 hunks)
✅ Files skipped from review due to trivial changes (5)
- tests/snapshots/TestCLICommands_atmos_auth_merging.stdout.golden
- tests/snapshots/TestCLICommands_atmos_auth_merging.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_env_(CI_mode).stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_env_(no_defaults_-_CI).stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_env_--format_invalid.stderr.golden
🚧 Files skipped from review as they are similar to previous changes (8)
- tests/snapshots/TestCLICommands_atmos_auth_env_--format_dotenv_--identity_test-user.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_env_(multiple_defaults_-_CI).stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_env_--format_json_--identity_test-user.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_exec_--identity_test-user_--_echo_hello.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_env_--identity_readonly_(no_defaults).stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_login_--identity_test-user.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_exec_--help.stderr.golden
- internal/auth/hooks.go
🧰 Additional context used
📓 Path-based instructions (3)
tests/test-cases/**
📄 CodeRabbit inference engine (CLAUDE.md)
Never modify golden snapshot fixtures under tests/test-cases/ unless explicitly instructed.
Files:
tests/test-cases/auth-merge.yamltests/test-cases/auth-default-identity.yamltests/test-cases/auth-validation.yamltests/test-cases/auth-cli.yaml
**/*.{yaml,yml}
📄 CodeRabbit inference engine (CLAUDE.md)
YAML configs should support Go templating using provided template functions.
Files:
tests/test-cases/auth-merge.yamltests/fixtures/scenarios/atmos-auth/stacks/deploy/component-auth-configured.yamltests/test-cases/auth-default-identity.yamltests/fixtures/scenarios/atmos-auth-invalid/atmos.yamltests/test-cases/auth-validation.yamltests/test-cases/auth-cli.yamltests/fixtures/scenarios/atmos-auth/atmos.yamltests/fixtures/scenarios/atmos-auth/stacks/deploy/nonprod.yaml
**/*.go
📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)
**/*.go: Use Viper for managing configuration, environment variables, and flags
Use interfaces for external dependencies to facilitate mocking
All code must pass golangci-lint checks
Follow Go's error handling idioms
Use meaningful error messages
Wrap errors with context using fmt.Errorf("context: %w", err)
Consider using a custom error type for domain-specific errors
Follow standard Go coding style
Use gofmt and goimports to format code
Prefer short, descriptive variable names
Use snake_case for environment variables
Document all exported functions, types, and methods
Document complex logic with inline comments
Follow Go's documentation conventions
Use Viper for configuration management
Support configuration via files, environment variables, and flags
Follow the precedence order: flags > environment variables > config file > defaults
**/*.go: All comments in Go code must end with periods (enforced by golangci-lint godot).
Wrap all returned errors using static errors from the errors package; never return dynamic errors directly.
Always bind environment variables with viper.BindEnv() and provide ATMOS_ alternatives for each external var.
Separate structured logging from UI output: use stderr for prompts/errors to user; stdout only for data; never use logging for UI.
Most text UI must go to stderr; only data/results to stdout. Prefer utils.PrintfMessageToTUI for UI messages.
For non-standard execution paths, capture telemetry with telemetry.CaptureCmd or telemetry.CaptureCmdString and never capture user data.
Ensure cross-platform compatibility: prefer SDKs to external binaries, use filepath/os/runtime for portability, and add build constraints when needed.
Files:
internal/auth/types/github_oidc_credentials.gointernal/auth/types/aws_credentials.gointernal/auth/types/whoami.gopkg/schema/schema.gointernal/auth/identities/aws/permission_set.gointernal/auth/validation/validator.go
🧬 Code graph analysis (6)
internal/auth/types/github_oidc_credentials.go (1)
internal/auth/types/whoami.go (1)
WhoamiInfo(6-16)
internal/auth/types/aws_credentials.go (1)
internal/auth/types/whoami.go (1)
WhoamiInfo(6-16)
internal/auth/types/whoami.go (3)
pkg/schema/schema.go (2)
Provider(982-994)Identity(1002-1010)internal/auth/hooks.go (2)
Provider(18-18)Identity(19-19)internal/auth/types/interfaces.go (1)
ICredentials(171-177)
pkg/schema/schema.go (2)
internal/auth/hooks.go (2)
Identity(19-19)Provider(18-18)internal/auth/types/interfaces.go (2)
Identity(75-95)Provider(53-72)
internal/auth/identities/aws/permission_set.go (5)
pkg/schema/schema.go (5)
Identity(1002-1010)Context(371-386)Validate(184-186)Provider(982-994)ConfigAndStacksInfo(460-539)internal/auth/types/interfaces.go (3)
Identity(75-95)ICredentials(171-177)Provider(53-72)errors/errors.go (4)
ErrInvalidIdentityKind(84-84)ErrInvalidIdentityConfig(85-85)ErrAwsAuth(92-92)ErrAuthenticationFailed(88-88)internal/auth/types/aws_credentials.go (1)
AWSCredentials(6-13)internal/auth/cloud/aws/setup.go (2)
SetupFiles(13-38)SetEnvironmentVariables(41-50)
internal/auth/validation/validator.go (4)
internal/auth/hooks.go (3)
Validator(22-22)Provider(18-18)Identity(19-19)internal/auth/types/interfaces.go (3)
Validator(157-169)Provider(53-72)Identity(75-95)pkg/schema/schema.go (4)
AuthConfig(975-979)Logs(366-369)Provider(982-994)Identity(1002-1010)errors/errors.go (4)
ErrInvalidAuthConfig(83-83)ErrInvalidProviderConfig(87-87)ErrInvalidProviderKind(86-86)ErrInvalidIdentityConfig(85-85)
🪛 YAMLlint (1.37.1)
tests/fixtures/scenarios/atmos-auth/stacks/deploy/component-auth-configured.yaml
[error] 19-19: no new line character at the end of file
(new-line-at-end-of-file)
[error] 19-19: trailing spaces
(trailing-spaces)
tests/fixtures/scenarios/atmos-auth-invalid/atmos.yaml
[error] 13-13: trailing spaces
(trailing-spaces)
tests/fixtures/scenarios/atmos-auth/atmos.yaml
[error] 10-10: trailing spaces
(trailing-spaces)
[error] 16-16: trailing spaces
(trailing-spaces)
[error] 21-21: trailing spaces
(trailing-spaces)
[error] 39-39: trailing spaces
(trailing-spaces)
[error] 48-48: trailing spaces
(trailing-spaces)
[error] 55-55: trailing spaces
(trailing-spaces)
[error] 58-58: trailing spaces
(trailing-spaces)
tests/fixtures/scenarios/atmos-auth/stacks/deploy/nonprod.yaml
[error] 9-9: no new line character at the end of file
(new-line-at-end-of-file)
🪛 GitHub Check: golangci-lint
internal/auth/validation/validator.go
[failure] 1-1:
: # github.com/cloudposse/atmos/internal/auth/validation
internal/auth/validation/validator.go:111:2: declared and not used: i
internal/auth/validation/validator.go:111:8: invalid operation: identity (variable of type *schema.Identity) is not an interface
🪛 GitHub Check: Build (windows-latest, windows)
internal/auth/validation/validator.go
[failure] 111-111:
invalid operation: identity (variable of type *schema.Identity) is not an interface
[failure] 111-111:
declared and not used: i
🪛 GitHub Check: Build (macos-latest, macos)
internal/auth/validation/validator.go
[failure] 111-111:
invalid operation: identity (variable of type *schema.Identity) is not an interface
[failure] 111-111:
declared and not used: i
🪛 GitHub Check: Build (ubuntu-latest, linux)
internal/auth/validation/validator.go
[failure] 111-111:
invalid operation: identity (variable of type *schema.Identity) is not an interface
[failure] 111-111:
declared and not used: i
🪛 GitHub Actions: Tests
internal/auth/validation/validator.go
[error] 111-111: Go build error: 'i' declared and not used (validator.go:111). Command: env GOOS=darwin go build -o build/atmos -v -ldflags -X 'github.com/cloudposse/atmos/pkg/version.Version=test'
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Summary
🔇 Additional comments (5)
tests/fixtures/scenarios/atmos-auth/stacks/deploy/nonprod.yaml (1)
1-9: LGTM for a “no component auth” fixture.Reads clean and matches intent.
tests/test-cases/auth-validation.yaml (2)
40-56: Test expectation may mask a real validation error (missing provider).This case expects exit_code 0, but the validator should fail when an identity references a non-existent provider. If a feature flag (ATMOS_AUTH_TEST_MISSING_PROVIDER) bypasses the check, document it in the test name/description or assert a warning on stderr.
58-74: Same concern for circular dependency test.Expecting success for a circular chain contradicts the validator’s cycle detection. If gated by ATMOS_AUTH_TEST_CIRCULAR, make that explicit in the test expectation (e.g., expect a warning).
internal/auth/validation/validator.go (1)
21-21: Doc comments end with periods (already good) — no action.Also applies to: 68-68, 92-92, 127-127, 155-155, 170-170, 188-188, 197-197, 221-221, 239-239, 245-245, 264-265
pkg/schema/schema.go (1)
60-60: LGTM: adding Auth to AtmosConfiguration.Shape and tags look consistent with the rest of the schema.
| principalName = "name" | ||
| principalAccount = "account" | ||
| principalAccountName = "name" | ||
| principalAccountID = "id" | ||
| ) |
There was a problem hiding this comment.
Fix compile error and align account validation with Validate().
- principalAccountName is declared but never used; also the Authenticate() check incorrectly requires both name and id.
Apply:
- accountName, okAccountName := accountSpec[principalName].(string)
- accountID, okAccountID := accountSpec[principalAccountID].(string)
- if !(okAccountName || okAccountID) || accountName == "" || accountID == "" {
- return nil, fmt.Errorf("%w: account name/id is required", errUtils.ErrInvalidIdentityConfig)
- }
+ accountName, _ := accountSpec[principalAccountName].(string)
+ accountID, _ := accountSpec[principalAccountID].(string)
+ if accountName == "" && accountID == "" {
+ return nil, fmt.Errorf("%w: account name or account ID is required", errUtils.ErrInvalidIdentityConfig)
+ }Also applies to: 75-79
🤖 Prompt for AI Agents
internal/auth/identities/aws/permission_set.go around lines 19-23 (and likewise
update lines ~75-79): remove the unused constant principalAccountName (it
duplicates principalName) to fix the compile error, and change the
Authenticate() account validation so it does not require both account name and
id; instead require at least one (account name OR account id) consistent with
Validate(). Update the duplicate validation block at lines ~75-79 the same way.
| Credentials ICredentials `json:"credentials,omitempty"` | ||
| LastUpdated time.Time `json:"last_updated"` |
There was a problem hiding this comment.
Do not serialize credentials; risk of secret leakage and incompatible JSON.
ICredentials may carry tokens/keys; exposing via JSON is dangerous and may not marshal cleanly. Omit from JSON (and YAML) and let BuildWhoamiInfo project safe fields.
- Credentials ICredentials `json:"credentials,omitempty"`
+ Credentials ICredentials `json:"-"`📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Credentials ICredentials `json:"credentials,omitempty"` | |
| LastUpdated time.Time `json:"last_updated"` | |
| // internal/auth/types/whoami.go | |
| type WhoamiInfo struct { | |
| // ... other fields ... | |
| // Credentials should not be serialized to JSON/YAML to avoid leaking secrets. | |
| Credentials ICredentials `json:"-"` | |
| LastUpdated time.Time `json:"last_updated"` | |
| } |
🤖 Prompt for AI Agents
internal/auth/types/whoami.go around lines 14-15: the Credentials field must not
be serialized; change its struct tags to omit it from JSON and YAML (e.g.
json:"-" yaml:"-") so secrets/tokens are never marshaled, and ensure callers
rely on BuildWhoamiInfo to project only safe fields rather than expecting
credentials to appear in serialized output.
There was a problem hiding this comment.
This is required for unmarshaling credentials from the keystore
There was a problem hiding this comment.
@Benbentwo I understand the keystore unmarshaling requirement. You're right that if credentials need to be persisted and retrieved from the keystore, they must be serializable.
However, the security concern remains valid - this struct could inadvertently expose secrets if logged, returned in API responses, or stored inappropriately. Here are some mitigation suggestions:
- Add security documentation to the struct comment warning about credential exposure
- Consider separate types - use one internal type for keystore operations and another for user-facing APIs
- Careful handling - ensure WhoamiInfo instances are never logged or exposed in debug output
- Custom marshaling - implement MarshalJSON() to conditionally exclude credentials in certain contexts
Would you consider adding a comment to document this security consideration, such as:
// WhoamiInfo represents the current effective authentication principal.
// WARNING: Contains sensitive credential data. Handle with care to avoid
// accidental exposure in logs, API responses, or debug output.
type WhoamiInfo struct {
// ... fields ...
Credentials ICredentials `json:"credentials,omitempty"` // Required for keystore unmarshaling
}This preserves the functionality while alerting future developers to the security implications.
✏️ Learnings added
Learnt from: Benbentwo
PR: cloudposse/atmos#1452
File: internal/auth/types/whoami.go:14-15
Timestamp: 2025-09-09T02:14:36.682Z
Learning: The WhoamiInfo struct in internal/auth/types/whoami.go requires the Credentials field to be JSON-serializable for keystore unmarshaling operations, despite security concerns about credential exposure.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| @@ -0,0 +1,282 @@ | |||
| package validation | |||
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Follow-ups: repo-wide checks.
Run to ensure no remaining double-%w and no stale type assertions:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Double-%w checks:"
rg -nP 'fmt\.Errorf\([^)]*%w[^)]*%w' internal || true
echo
echo "Stale type assertions on schema.Identity:"
rg -nP '\*schema\.Identity\)\.\(' internal || trueLength of output: 1907
Remove remaining double-%w error wraps
Only one %w is allowed per fmt.Errorf. Instances still present (line numbers in repo):
- internal/auth/validation/validator.go:31, 39, 45, 182
- internal/auth/providers/github/oidc.go:100, 124, 136, 146
- internal/auth/manager.go:513
- internal/auth/identities/aws/user.go:79
- internal/auth/cloud/aws/files.go:49
- internal/exec/yaml_func_store_get.go:98
- internal/exec/terraform_generate_planfile.go:219
No stale type assertions on schema.Identity detected.
🧰 Tools
🪛 GitHub Check: golangci-lint
[failure] 1-1:
: # github.com/cloudposse/atmos/internal/auth/validation
internal/auth/validation/validator.go:111:2: declared and not used: i
internal/auth/validation/validator.go:111:8: invalid operation: identity (variable of type *schema.Identity) is not an interface
🤖 Prompt for AI Agents
internal/auth/validation/validator.go lines ~31,39,45,182: several fmt.Errorf
calls currently use multiple %w verbs; change each to use at most one %w by
keeping one wrapped error as %w and formatting any additional errors with %v (or
combine multiple errors with errors.Join and wrap that single joined error with
%w), e.g., replace patterns that have two or more %w with a single %w and use %v
for the rest so fmt.Errorf compiles and preserves error context.
| func (v *validator) validateSAMLProvider(provider *schema.Provider) error { | ||
| if provider.URL == "" { | ||
| return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig) | ||
| } | ||
|
|
||
| if provider.Region == "" { | ||
| return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig) | ||
| } | ||
|
|
||
| // Validate URL format | ||
| if _, err := url.Parse(provider.URL); err != nil { | ||
| return fmt.Errorf("%w: invalid URL format: %w", errUtils.ErrInvalidAuthConfig, err) | ||
| } | ||
|
|
||
| return nil | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Harden URL validation and fix multiple %w; use provider-scoped sentinel.
Ensure scheme/host exist and avoid double-wrapping.
if provider.URL == "" {
- return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig)
+ return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidProviderConfig)
}
if provider.Region == "" {
- return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig)
+ return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidProviderConfig)
}
-// Validate URL format
-if _, err := url.Parse(provider.URL); err != nil {
- return fmt.Errorf("%w: invalid URL format: %w", errUtils.ErrInvalidAuthConfig, err)
+// Validate URL format.
+u, err := url.ParseRequestURI(provider.URL)
+if err != nil || u.Scheme == "" || u.Host == "" {
+ return fmt.Errorf("%w: invalid URL format: %v", errUtils.ErrInvalidProviderConfig, provider.URL)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func (v *validator) validateSAMLProvider(provider *schema.Provider) error { | |
| if provider.URL == "" { | |
| return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig) | |
| } | |
| if provider.Region == "" { | |
| return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig) | |
| } | |
| // Validate URL format | |
| if _, err := url.Parse(provider.URL); err != nil { | |
| return fmt.Errorf("%w: invalid URL format: %w", errUtils.ErrInvalidAuthConfig, err) | |
| } | |
| return nil | |
| } | |
| func (v *validator) validateSAMLProvider(provider *schema.Provider) error { | |
| if provider.URL == "" { | |
| return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidProviderConfig) | |
| } | |
| if provider.Region == "" { | |
| return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidProviderConfig) | |
| } | |
| // Validate URL format. | |
| u, err := url.ParseRequestURI(provider.URL) | |
| if err != nil || u.Scheme == "" || u.Host == "" { | |
| return fmt.Errorf("%w: invalid URL format: %v", errUtils.ErrInvalidProviderConfig, provider.URL) | |
| } | |
| return nil | |
| } |
There was a problem hiding this comment.
Actionable comments posted: 10
♻️ Duplicate comments (20)
examples/demo-auth/atmos.yaml (2)
27-36: YAML list indentation under commands fixed.
This addresses the earlier yamllint warning.
38-41: Add default_region and session_length for completeness.
Matches PR objectives and helps demos behave consistently. Use Go duration syntax.Apply:
auth: logs: level: Debug + default_region: us-east-1 + session_length: 15mcmd/auth_exec.go (5)
18-26: Fix flag parsing to preserve --identity and pass-through args.
- Remove DisableFlagParsing; keep Cobra parsing so --identity works.
- Whitelist unknown flags so delegated command args are not rejected.
var authExecCmd = &cobra.Command{ Use: "exec", Short: "Execute a command with authentication environment variables.", Long: "Execute a command with the authenticated identity's environment variables set. Use `--` to separate Atmos flags from the command's native arguments.", Example: ` # Run terraform with the authenticated identity atmos auth exec -- terraform plan -var-file=env.tfvars`, - Args: cobra.MinimumNArgs(1), - DisableFlagParsing: true, - - FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.MinimumNArgs(1), + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true},
27-41: Add telemetry capture; avoid stack processing; wrap errors with sentinels.
- Capture command telemetry.
- Use InitCliConfig(..., false) for faster startup.
- Wrap with static errors per guidelines.
- RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) (retErr error) { + defer func() { telemetry.CaptureCmd(cmd, retErr) }() handleHelpRequest(cmd, args) checkAtmosConfig() // Load atmos configuration - atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, true) - if err != nil { - return fmt.Errorf("failed to load atmos config: %w", err) - } + atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false) + if err != nil { + return fmt.Errorf("%w: failed to load atmos config: %v", errUtils.ErrSubcommandFailed, err) + } // Create auth manager authManager, err := createAuthManager(&atmosConfig.Auth) if err != nil { - return fmt.Errorf("failed to create auth manager: %w", err) + return fmt.Errorf("%w: failed to create auth manager: %v", errUtils.ErrAuthManager, err) }
42-57: Use Viper for precedence; use cmd.Context; wrap with domain errors.
- Read identity via Viper (flags > env > config).
- Use cmd.Context for cancellation.
- Use domain-specific sentinels.
- // Get identity from flag or use default - identityName, _ := cmd.Flags().GetString("identity") + // Get identity from Viper (flags > env > config) or use default. + identityName := viper.GetString("identity") if identityName == "" { defaultIdentity, err := authManager.GetDefaultIdentity() if err != nil { - return fmt.Errorf("no default identity configured and no identity specified: %w", err) + return fmt.Errorf("%w: no default identity configured and no identity specified: %v", errUtils.ErrDefaultIdentity, err) } identityName = defaultIdentity } - // Authenticate and get environment variables - ctx := context.Background() + // Authenticate and get environment variables. + ctx := cmd.Context() whoami, err := authManager.Authenticate(ctx, identityName) if err != nil { - return fmt.Errorf("authentication failed: %w", err) + return fmt.Errorf("%w: authentication failed: %v", errUtils.ErrAuthenticationFailed, err) }
114-116: Bind --identity to Viper and env vars.Enable precedence and env override.
func init() { authExecCmd.Flags().StringP("identity", "i", "", "Specify the identity to use for authentication") + _ = viper.BindPFlag("identity", authExecCmd.Flags().Lookup("identity")) + viper.MustBindEnv("identity", "AUTH_IDENTITY", "ATMOS_IDENTITY") authCmd.AddCommand(authExecCmd) }
3-14: Add imports for Viper and telemetry.Required by changes above.
import ( "context" "fmt" "os" "os/exec" "syscall" errUtils "github.com/cloudposse/atmos/errors" cfg "github.com/cloudposse/atmos/pkg/config" "github.com/cloudposse/atmos/pkg/schema" "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/cloudposse/atmos/pkg/telemetry" )tests/fixtures/scenarios/atmos-auth/atmos.yaml (1)
11-21: Ensure JSON schemas expose auth.providers/identities.The fixture relies on top-level auth providers/identities; verify schemas in pkg/datafetcher/schema include these properties to avoid validation drift.
Also applies to: 22-66
cmd/auth_user.go (2)
26-35: Capture telemetry and wrap config load errors with sentinels.var authUserConfigureCmd = &cobra.Command{ Use: "configure", Short: "Configure static AWS user credentials (stored securely in keyring)", - Long: `Configure static AWS user credentials (stored securely in keyring)`, - RunE: func(cmd *cobra.Command, args []string) error { + Long: `Configure static AWS user credentials (stored securely in keyring).`, + RunE: func(cmd *cobra.Command, args []string) (retErr error) { + defer func() { telemetry.CaptureCmd(cmd, retErr) }() // Load atmos config atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false) if err != nil { - return err + return fmt.Errorf("%w: failed to load atmos config: %v", errUtils.ErrSubcommandFailed, err) }
89-109: Wrap interactive/store errors; send user-facing success to TUI, not logger.- if err := form.Run(); err != nil { - return err + if err := form.Run(); err != nil { + return fmt.Errorf("%w: credential input cancelled or failed: %v", errUtils.ErrSubcommandFailed, err) } ... - if err := store.Store(alias, creds); err != nil { - return err + if err := store.Store(alias, creds); err != nil { + return fmt.Errorf("%w: failed to store credentials: %v", errUtils.ErrSubcommandFailed, err) } - log.Info("Saved credentials to keyring", "alias", alias) + utils.PrintfMessageToTUI("Saved credentials to keyring. Alias: %s.", alias) return nilcmd/auth_env.go (5)
26-31: Use Viper for --format precedence; validate against SupportedFormats.- // Get output format - format, _ := cmd.Flags().GetString("format") + // Get output format. + format := viper.GetString("auth_env_format") + if format == "" { + format = "export" + } if !slices.Contains(SupportedFormats, format) { - return fmt.Errorf("%w invalid format: %s", errUtils.ErrInvalidArgumentError, format) + return fmt.Errorf("%w: invalid format: %s", errUtils.ErrInvalidArgumentError, format) }
44-52: Resolve identity via Viper; wrap default-identity miss.- // Get identity from flag or use default - identityName, _ := cmd.Flags().GetString("identity") + // Get identity from Viper (flags > env > config) or use default. + identityName := viper.GetString("identity") if identityName == "" { defaultIdentity, err := authManager.GetDefaultIdentity() if err != nil { - return fmt.Errorf("no default identity configured and no identity specified: %w", err) + return fmt.Errorf("%w: no default identity configured and no identity specified: %v", errUtils.ErrDefaultIdentity, err) } identityName = defaultIdentity }
54-59: Use cmd.Context and sentinel for auth failure.- // Authenticate and get environment variables - ctx := context.Background() + // Authenticate and get environment variables. + ctx := cmd.Context() whoami, err := authManager.Authenticate(ctx, identityName) if err != nil { - return fmt.Errorf("authentication failed: %w", err) + return fmt.Errorf("%w: authentication failed: %v", errUtils.ErrAuthenticationFailed, err) }
101-111: Bind identity to Viper correctly (parent persistent flag or local fallback).Avoid nil BindPFlag when the flag is defined on the parent.
_ = viper.BindPFlag("auth_env_format", authEnvCmd.Flags().Lookup("format")) _ = authEnvCmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return SupportedFormats, cobra.ShellCompDirectiveNoFileComp }) - _ = viper.BindPFlag("identity", authEnvCmd.Flags().Lookup("identity")) + if f := authCmd.PersistentFlags().Lookup("identity"); f != nil { + _ = viper.BindPFlag("identity", f) + } else { + authEnvCmd.Flags().StringP("identity", "i", "", "Specify the identity to use for authentication.") + _ = viper.BindPFlag("identity", authEnvCmd.Flags().Lookup("identity")) + } viper.MustBindEnv("identity", "AUTH_IDENTITY", "ATMOS_IDENTITY") authCmd.AddCommand(authEnvCmd)
33-36: Wrap config load with sentinel.- // Load atmos configuration + // Load atmos configuration. atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, true) if err != nil { - return fmt.Errorf("failed to load atmos config: %w", err) + return fmt.Errorf("%w: failed to load atmos config: %v", errUtils.ErrSubcommandFailed, err) }internal/auth/validation/validator.go (5)
220-236: Loosen assume-role ARN validation to support all partitions; identity-scoped sentinel.roleArn, ok := identity.Principal["assume_role"].(string) if !ok || roleArn == "" { return fmt.Errorf("%w: assume_role is required in principal", errUtils.ErrInvalidAuthConfig) } - if !strings.HasPrefix(roleArn, "arn:aws:iam::") { - return fmt.Errorf("%w: invalid role ARN format", errUtils.ErrInvalidAuthConfig) - } + // Accept aws, aws-us-gov, aws-cn, etc. + if !(strings.HasPrefix(roleArn, "arn:aws") && strings.Contains(roleArn, ":iam::") && strings.Contains(roleArn, ":role/")) { + return fmt.Errorf("%w: invalid role ARN format %q", errUtils.ErrInvalidIdentityConfig, roleArn) + }
153-164: Provider validators should use ErrInvalidProviderConfig.if provider.StartURL == "" { - return fmt.Errorf("%w: start_url is required for AWS SSO provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: start_url is required for AWS SSO provider", errUtils.ErrInvalidProviderConfig) } if provider.Region == "" { - return fmt.Errorf("%w: region is required for AWS SSO provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: region is required for AWS SSO provider", errUtils.ErrInvalidProviderConfig) }
168-184: Harden SAML URL validation; fix double-%w; provider-scoped sentinel.-// validateSAMLProvider validates AWS SAML provider configuration. +// validateSAMLProvider validates AWS SAML provider configuration. func (v *validator) validateSAMLProvider(provider *schema.Provider) error { if provider.URL == "" { - return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidProviderConfig) } if provider.Region == "" { - return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidProviderConfig) } - // Validate URL format - if _, err := url.Parse(provider.URL); err != nil { - return fmt.Errorf("%w: invalid URL format: %w", errUtils.ErrInvalidAuthConfig, err) - } + // Validate URL format. + u, err := url.ParseRequestURI(provider.URL) + if err != nil || u.Scheme == "" || u.Host == "" { + return fmt.Errorf("%w: invalid URL format %q", errUtils.ErrInvalidProviderConfig, provider.URL) + } return nil }
125-136: Validate referenced identities exist when building the graph.Prevents dangling edges before cycle detection.
for name, identity := range identities { if identity.Via != nil { if identity.Via.Identity != "" { + if _, ok := identities[identity.Via.Identity]; !ok { + return fmt.Errorf("%w: referenced identity %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Identity) + } graph[name] = append(graph[name], identity.Via.Identity) } } }
21-49: Validate logs; avoid multiple %w; precompute pointer maps.
- Call ValidateLogsConfig.
- Use only one %w per fmt.Errorf.
- Convert providers/identities once.
func (v *validator) ValidateAuthConfig(config *schema.AuthConfig) error { if config == nil { return fmt.Errorf("%w: auth config cannot be nil", errUtils.ErrInvalidAuthConfig) } - // Validate providers + // Validate logs. + if err := v.ValidateLogsConfig(&config.Logs); err != nil { + return fmt.Errorf("%w: logs configuration validation failed: %v", errUtils.ErrInvalidAuthConfig, err) + } + + // Precompute pointer maps. + providersPtr := convertProviders(config.Providers) + identitiesPtr := convertIdentities(config.Identities) + + // Validate providers. //nolint:gocritic // rangeValCopy: map stores structs; address of map element can't be taken. Passing copy to factory is intended. for name, provider := range config.Providers { if err := v.ValidateProvider(name, &provider); err != nil { - return fmt.Errorf("%w: provider %q validation failed: %w", errUtils.ErrInvalidAuthConfig, name, err) + return fmt.Errorf("%w: provider %q validation failed: %v", errUtils.ErrInvalidAuthConfig, name, err) } } - // Validate identities - + // Validate identities. for name, identity := range config.Identities { - if err := v.ValidateIdentity(name, &identity, convertProviders(config.Providers)); err != nil { - return fmt.Errorf("%w: identity %q validation failed: %w", errUtils.ErrInvalidAuthConfig, name, err) + if err := v.ValidateIdentity(name, &identity, providersPtr); err != nil { + return fmt.Errorf("%w: identity %q validation failed: %v", errUtils.ErrInvalidAuthConfig, name, err) } } - // Validate chains - if err := v.ValidateChains(convertIdentities(config.Identities), convertProviders(config.Providers)); err != nil { - return fmt.Errorf("%w: identity chain validation failed: %w", errUtils.ErrInvalidAuthConfig, err) + // Validate chains. + if err := v.ValidateChains(identitiesPtr, providersPtr); err != nil { + return fmt.Errorf("%w: identity chain validation failed: %v", errUtils.ErrInvalidAuthConfig, err) } return nil }
🧹 Nitpick comments (18)
examples/demo-auth/atmos.yaml (1)
81-86: Disabled user identity is fine; consider clarity.
Optionally add a comment noting where credentials are sourced (env vars vs credential store) or a placeholder profile.superuser: enabled: false kind: aws/user credentials: region: us-east-1 + # credentials picked up from env or credential store when enabled + # profile: superusertests/snapshots/TestCLICommands_atmos_describe_config.stdout.golden (1)
188-196: Use nulls (or omit) for unset auth.logs fields for consistencyYou emit empty strings for logs.file/level while providers/identities are null. Prefer null (or omit via
omitempty) to avoid ambiguous “empty” values and keep type semantics consistent.Apply to snapshot after adjusting serialization:
- "logs": { - "file": "", - "level": "" - }, + "logs": { + "file": null, + "level": null + },And in Go (outside this file), consider:
type LogsConfig struct { File *string `json:"file,omitempty"` Level *string `json:"level,omitempty"` }cmd/auth_exec.go (1)
70-71: Lint: end comment with a period.Add trailing period to satisfy godot.
-// executeCommandWithEnv executes a command with additional environment variables +// executeCommandWithEnv executes a command with additional environment variables.tests/fixtures/scenarios/atmos-auth/atmos.yaml (1)
10-10: Trim trailing spaces to satisfy YAML linters.Whitespace-only lines with spaces trip YAMLlint. Replace with truly empty lines.
- + - + - + - + - + - + - +Also applies to: 16-16, 21-21, 39-39, 48-48, 55-55, 58-58
cmd/auth_user.go (3)
18-23: Polish help text and follow style.Capitalize AWS/Atmos and end sentences with periods.
// authUserCmd groups user-related auth commands. var authUserCmd = &cobra.Command{ Use: "user", - Short: "Manage aws user credentials for atmos auth", - Long: `Manage AWS user credentials for Atmos authentication.`, + Short: "Manage AWS user credentials for Atmos auth.", + Long: `Manage AWS user credentials for Atmos authentication.`, }
37-55: Prefer default identity when marked; keep error sentinel.Use the identity with Default: true if present.
// Gather identities that use a provider of type aws/user. var selectable []string if atmosConfig.Auth.Identities == nil { return fmt.Errorf("%w: no auth identities configured in atmos.yaml", errUtils.ErrInvalidAuthConfig) } defaultChoice := "" for ident := range atmosConfig.Auth.Identities { identity := atmosConfig.Auth.Identities[ident] if identity.Kind == "aws/user" { selectable = append(selectable, ident) if identity.Default && defaultChoice == "" { defaultChoice = ident } } } if len(selectable) == 0 { return fmt.Errorf("%w: no identities configured for provider type 'aws/user'. Define one under auth.identities in atmos.yaml", errUtils.ErrInvalidAuthConfig) } // Choose identity - choice := selectable[0] + choice := selectable[0] + if defaultChoice != "" { + choice = defaultChoice + }
3-16: Update imports for telemetry/utils; drop unused logger.import ( "fmt" "github.com/charmbracelet/huh" - log "github.com/charmbracelet/log" errUtils "github.com/cloudposse/atmos/errors" "github.com/cloudposse/atmos/internal/auth/credentials" "github.com/cloudposse/atmos/internal/auth/types" uiutils "github.com/cloudposse/atmos/internal/tui/utils" cfg "github.com/cloudposse/atmos/pkg/config" "github.com/cloudposse/atmos/pkg/schema" + "github.com/cloudposse/atmos/pkg/telemetry" + utils "github.com/cloudposse/atmos/pkg/utils" "github.com/spf13/cobra" )cmd/auth_env.go (4)
18-23: End comments with periods; polish help text.-// authEnvCmd exports authentication environment variables +// authEnvCmd exports authentication environment variables. var authEnvCmd = &cobra.Command{ Use: "env", - Short: "Export authentication environment variables", - Long: "Export environment variables for the authenticated identity to use with external tools.", + Short: "Export authentication environment variables.", + Long: "Export environment variables for the authenticated identity to use with external tools.",
80-84: Lint: end comment with a period.-// outputEnvAsJSON outputs environment variables as JSON +// outputEnvAsJSON outputs environment variables as JSON.
85-91: Lint: end comment with a period.-// outputEnvAsExport outputs environment variables as shell export statements +// outputEnvAsExport outputs environment variables as shell export statements.
93-99: Lint: end comment with a period.-// outputEnvAsDotenv outputs environment variables in .env format +// outputEnvAsDotenv outputs environment variables in .env format.internal/auth/validation/validator.go (2)
186-193: Use provider-scoped sentinel for GitHub OIDC region.if provider.Region == "" { - return fmt.Errorf("%w: region is required for GitHub OIDC provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: region is required for GitHub OIDC provider", errUtils.ErrInvalidProviderConfig) }
78-78: Lint: add periods to TODO comments.-// TODO replace with Provider Interface Validate() +// TODO replace with Provider Interface Validate(). ... -// TODO replace with Identity Interface Validate() +// TODO replace with Identity Interface Validate().Also applies to: 111-112
cmd/auth_whoami.go (5)
43-49: No default identity: consider non-zero exit + hint code.Optional: return a typed error (e.g., errors.ErrNoDefaultIdentity) so scripts can detect this via exit code, and add a troubleshooting hint (e.g.,
atmos auth user configure).
51-56: No active session: consider non-zero exit.Same note as above; a typed error (e.g., errors.ErrNoActiveSession) improves automation.
79-96: TUI output is fine; consider using utils.PrintfMessageToTUI for consistency.Keeps UI on stderr but standardizes styling across commands.
100-113: Make path redaction more robust across platforms + fix comment period.Normalize paths before comparing.
-// redactHomeDir replaces occurrences of the homeDir at the start of v with "~" to avoid leaking user paths +// redactHomeDir replaces occurrences of the homeDir at the start of v with "~" to avoid leaking user paths. func redactHomeDir(v string, homeDir string) string { - if homeDir == "" { - return v - } - // Ensure both have the same path separator - if strings.HasPrefix(v, homeDir+string(os.PathSeparator)) { - return "~" + v[len(homeDir):] - } - if v == homeDir { - return "~" - } - return v + if homeDir == "" || v == "" { + return v + } + hv := filepath.Clean(v) + hh := filepath.Clean(homeDir) + if strings.HasPrefix(hv, hh+string(os.PathSeparator)) { + return "~" + hv[len(hh):] + } + if hv == hh { + return "~" + } + return v }Add import:
import ( "context" "encoding/json" "fmt" "os" "strings" + "path/filepath" )
42-55: Support--identityoverride in whoami
Checkviper.GetString("auth.whoami.identity")first and fall back toGetDefaultIdentity(), then pass that name directly toWhoami.Example change:
ctx := context.Background() name := viper.GetString("auth.whoami.identity") if name == "" { var err error name, err = authManager.GetDefaultIdentity() if err != nil { fmt.Fprintln(os.Stderr, "No default identity configured.") fmt.Fprintln(os.Stderr, "Configure auth in atmos.yaml and run `atmos auth login` to authenticate.") return nil } } whoami, err := authManager.Whoami(ctx, name) if err != nil { fmt.Fprintln(os.Stderr, "No active authentication session found.") fmt.Fprintln(os.Stderr, "Run `atmos auth login` to authenticate.") return nil }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (21)
.github/workflows/test.yml(4 hunks)cmd/auth_env.go(1 hunks)cmd/auth_exec.go(1 hunks)cmd/auth_user.go(1 hunks)cmd/auth_whoami.go(1 hunks)errors/errors.go(1 hunks)examples/demo-auth/atmos.yaml(1 hunks)internal/auth/validation/validator.go(1 hunks)tests/fixtures/scenarios/atmos-auth/atmos.yaml(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_(CI_mode).stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--format_dotenv_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--format_export_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--format_invalid.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--format_json_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_exec_--help.stdout.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_exec_--identity_test-user_--_echo_hello.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_login_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_merging.stdout.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_user_configure_--help.stdout.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_describe_config.stdout.golden(1 hunks)tests/test-cases/auth-cli.yaml(1 hunks)
✅ Files skipped from review due to trivial changes (4)
- tests/snapshots/TestCLICommands_atmos_auth_exec_--help.stdout.golden
- tests/snapshots/TestCLICommands_atmos_auth_env_(CI_mode).stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_login_--identity_test-user.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_exec_--identity_test-user_--_echo_hello.stderr.golden
🚧 Files skipped from review as they are similar to previous changes (6)
- tests/test-cases/auth-cli.yaml
- tests/snapshots/TestCLICommands_atmos_auth_env_--format_invalid.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_env_--format_dotenv_--identity_test-user.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_env_--format_export_--identity_test-user.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_user_configure_--help.stdout.golden
- .github/workflows/test.yml
🧰 Additional context used
📓 Path-based instructions (5)
cmd/*.go
📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)
cmd/*.go: Implement each Cobra command in a separate file under the cmd/ directory
Use kebab-case for command-line flags
Provide comprehensive help text for all commands and flags
Include examples in command help
Provide meaningful feedback to users in command implementation
Include progress indicators for long-running operations
Provide clear error messages to users
Include troubleshooting hints when appropriate
Log detailed errors for debugging
Files:
cmd/auth_whoami.gocmd/auth_env.gocmd/auth_exec.gocmd/auth_user.go
**/*.go
📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)
**/*.go: Use Viper for managing configuration, environment variables, and flags
Use interfaces for external dependencies to facilitate mocking
All code must pass golangci-lint checks
Follow Go's error handling idioms
Use meaningful error messages
Wrap errors with context using fmt.Errorf("context: %w", err)
Consider using a custom error type for domain-specific errors
Follow standard Go coding style
Use gofmt and goimports to format code
Prefer short, descriptive variable names
Use snake_case for environment variables
Document all exported functions, types, and methods
Document complex logic with inline comments
Follow Go's documentation conventions
Use Viper for configuration management
Support configuration via files, environment variables, and flags
Follow the precedence order: flags > environment variables > config file > defaults
**/*.go: All comments in Go code must end with periods (enforced by golangci-lint godot).
Wrap all returned errors using static errors from the errors package; never return dynamic errors directly.
Always bind environment variables with viper.BindEnv() and provide ATMOS_ alternatives for each external var.
Separate structured logging from UI output: use stderr for prompts/errors to user; stdout only for data; never use logging for UI.
Most text UI must go to stderr; only data/results to stdout. Prefer utils.PrintfMessageToTUI for UI messages.
For non-standard execution paths, capture telemetry with telemetry.CaptureCmd or telemetry.CaptureCmdString and never capture user data.
Ensure cross-platform compatibility: prefer SDKs to external binaries, use filepath/os/runtime for portability, and add build constraints when needed.
Files:
cmd/auth_whoami.gointernal/auth/validation/validator.goerrors/errors.gocmd/auth_env.gocmd/auth_exec.gocmd/auth_user.go
cmd/**/!(*_test).go
📄 CodeRabbit inference engine (CLAUDE.md)
cmd/**/!(*_test).go: One Cobra command per file in cmd/.
Use //go:embed to load *_usage.md examples and utils.PrintfMarkdown() to render them in commands.
Files:
cmd/auth_whoami.gocmd/auth_env.gocmd/auth_exec.gocmd/auth_user.go
errors/errors.go
📄 CodeRabbit inference engine (CLAUDE.md)
Define all static errors in errors/errors.go.
Files:
errors/errors.go
**/*.{yaml,yml}
📄 CodeRabbit inference engine (CLAUDE.md)
YAML configs should support Go templating using provided template functions.
Files:
examples/demo-auth/atmos.yamltests/fixtures/scenarios/atmos-auth/atmos.yaml
🧠 Learnings (1)
📚 Learning: 2025-09-09T02:14:36.682Z
Learnt from: Benbentwo
PR: cloudposse/atmos#1452
File: internal/auth/types/whoami.go:14-15
Timestamp: 2025-09-09T02:14:36.682Z
Learning: The WhoamiInfo struct in internal/auth/types/whoami.go requires the Credentials field to be JSON-serializable for keystore unmarshaling operations, despite security concerns about credential exposure.
Applied to files:
cmd/auth_whoami.go
🧬 Code graph analysis (5)
cmd/auth_whoami.go (3)
pkg/schema/schema.go (4)
Command(645-655)ConfigAndStacksInfo(460-539)Provider(982-994)Identity(1002-1010)pkg/config/config.go (1)
InitCliConfig(25-62)pkg/config/go-homedir/homedir.go (1)
Dir(36-63)
internal/auth/validation/validator.go (3)
internal/auth/types/interfaces.go (3)
Validator(157-169)Provider(53-72)Identity(75-95)pkg/schema/schema.go (4)
AuthConfig(975-979)Logs(366-369)Provider(982-994)Identity(1002-1010)errors/errors.go (4)
ErrInvalidAuthConfig(83-83)ErrInvalidProviderConfig(87-87)ErrInvalidProviderKind(86-86)ErrInvalidIdentityConfig(85-85)
cmd/auth_env.go (4)
pkg/schema/schema.go (3)
Command(645-655)ConfigAndStacksInfo(460-539)AtmosConfiguration(25-61)errors/errors.go (1)
ErrInvalidArgumentError(99-99)pkg/config/config.go (1)
InitCliConfig(25-62)pkg/utils/json_utils.go (1)
PrintAsJSON(16-23)
cmd/auth_exec.go (3)
pkg/config/config.go (1)
InitCliConfig(25-62)pkg/schema/schema.go (1)
ConfigAndStacksInfo(460-539)errors/errors.go (2)
ErrInvalidSubcommand(96-96)ErrSubcommandFailed(97-97)
cmd/auth_user.go (6)
pkg/schema/schema.go (2)
Command(645-655)ConfigAndStacksInfo(460-539)pkg/config/config.go (1)
InitCliConfig(25-62)errors/errors.go (1)
ErrInvalidAuthConfig(83-83)internal/tui/utils/utils.go (1)
NewAtmosHuhTheme(85-93)internal/auth/credentials/store.go (1)
NewCredentialStore(29-31)internal/auth/types/aws_credentials.go (1)
AWSCredentials(6-13)
🪛 GitHub Check: golangci-lint
cmd/auth_whoami.go
[failure] 17-17:
Comment should end in a period
[failure] 24-24:
unlambda: replace func(cmd *cobra.Command, args []string) error { return executeAuthWhoamiCommand(cmd, args) } with executeAuthWhoamiCommand
cmd/auth_env.go
[failure] 18-18:
Comment should end in a period
[failure] 80-80:
Comment should end in a period
[failure] 85-85:
Comment should end in a period
[failure] 93-93:
Comment should end in a period
cmd/auth_exec.go
[failure] 16-16:
Comment should end in a period
[failure] 70-70:
Comment should end in a period
[failure] 105-105:
deep-exit: calls to os.Exit only in main() or init() functions
cmd/auth_user.go
[failure] 76-76:
do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf("required")"
[failure] 80-80:
SA1019: huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).Password is deprecated: use EchoMode(EchoPassword) instead.
[failure] 82-82:
do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf("required")"
🪛 GitHub Check: CodeQL
cmd/auth_whoami.go
[failure] 75-75: Clear-text logging of sensitive information
Sensitive data returned by an access to passwdParts flows to a logging call.
🪛 YAMLlint (1.37.1)
tests/fixtures/scenarios/atmos-auth/atmos.yaml
[error] 10-10: trailing spaces
(trailing-spaces)
[error] 16-16: trailing spaces
(trailing-spaces)
[error] 21-21: trailing spaces
(trailing-spaces)
[error] 39-39: trailing spaces
(trailing-spaces)
[error] 48-48: trailing spaces
(trailing-spaces)
[error] 55-55: trailing spaces
(trailing-spaces)
[error] 58-58: trailing spaces
(trailing-spaces)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: [mock-windows] tests/fixtures/scenarios/complete
- GitHub Check: [localstack] demo-localstack
- GitHub Check: Acceptance Tests (macos-latest, macos)
- GitHub Check: Acceptance Tests (windows-latest, windows)
- GitHub Check: Acceptance Tests (ubuntu-latest, linux)
- GitHub Check: Summary
🔇 Additional comments (10)
examples/demo-auth/atmos.yaml (6)
1-6: Schemas path and base look good.
Relative path resolves to examples/demo-auth/schemas/atmos-manifest.json given this file’s location.
7-14: Terraform component settings read clean.
Flags are explicit and sane for demos.
15-22: Stacks include/exclude patterns look correct.
Common _defaults.yaml is excluded; name pattern is simple.
43-56: Provider kinds and URL keys align with implementation.
github/oidc + spec.audience, aws/saml+url, and iam-identity-center+start_url look consistent.
57-65: No changes required for identity schema keys
The parser expects via.provider and principal.assume_role exactly as used in your snippet; nothing to update.
23-26: No changes needed for log level casing
log.ParseLevelusesstrings.ToLoweron the input and will correctly parse"Info","info", or any case variant. (docs-go.hexacode.org)errors/errors.go (1)
83-100: LGTM — well-scoped sentinel errors.The new error constants are cohesive and align with the CLI/auth domains.
tests/snapshots/TestCLICommands_atmos_auth_env_--format_json_--identity_test-user.stderr.golden (1)
2-6: LGTM — stderr snapshot matches error-path behavior.The multi-wrapped authentication failure lines look consistent with sentinel wrapping.
tests/snapshots/TestCLICommands_atmos_auth_merging.stdout.golden (1)
1-55: LGTM — merged auth view reads clean.Snapshot reflects identity defaults, via links, and providers as expected.
cmd/auth_whoami.go (1)
31-41: Config load and manager wiring LGTM.
| // Run the command and wait for completion | ||
| err = execCmd.Run() | ||
| if err != nil { | ||
| // If it's an exit error, preserve the exit code | ||
| if exitError, ok := err.(*exec.ExitError); ok { | ||
| if status, ok := exitError.Sys().(syscall.WaitStatus); ok { | ||
| os.Exit(status.ExitStatus()) | ||
| } | ||
| } | ||
| return fmt.Errorf("%w: command execution failed: %v", errUtils.ErrSubcommandFailed, err) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Avoid os.Exit in library code; return an error carrying the exit code.
Replace deep exit with a wrapped error to satisfy linters and allow the root to decide exit behavior.
err = execCmd.Run()
if err != nil {
// If it's an exit error, preserve the exit code
if exitError, ok := err.(*exec.ExitError); ok {
if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
- os.Exit(status.ExitStatus())
+ return fmt.Errorf("%w: subcommand exited with status %d", errUtils.ErrSubcommandFailed, status.ExitStatus())
}
}
return fmt.Errorf("%w: command execution failed: %v", errUtils.ErrSubcommandFailed, err)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Run the command and wait for completion | |
| err = execCmd.Run() | |
| if err != nil { | |
| // If it's an exit error, preserve the exit code | |
| if exitError, ok := err.(*exec.ExitError); ok { | |
| if status, ok := exitError.Sys().(syscall.WaitStatus); ok { | |
| os.Exit(status.ExitStatus()) | |
| } | |
| } | |
| return fmt.Errorf("%w: command execution failed: %v", errUtils.ErrSubcommandFailed, err) | |
| // Run the command and wait for completion | |
| err = execCmd.Run() | |
| if err != nil { | |
| // If it's an exit error, preserve the exit code by returning it instead of calling os.Exit. | |
| if exitError, ok := err.(*exec.ExitError); ok { | |
| if status, ok := exitError.Sys().(syscall.WaitStatus); ok { | |
| return fmt.Errorf("%w: subcommand exited with status %d", errUtils.ErrSubcommandFailed, status.ExitStatus()) | |
| } | |
| } | |
| return fmt.Errorf("%w: command execution failed: %v", errUtils.ErrSubcommandFailed, err) | |
| } |
🧰 Tools
🪛 GitHub Check: golangci-lint
[failure] 105-105:
deep-exit: calls to os.Exit only in main() or init() functions
🤖 Prompt for AI Agents
In cmd/auth_exec.go around lines 99 to 108, the code calls os.Exit when
encountering an exec.ExitError; instead of exiting in library code, extract the
exit status (status.ExitStatus()) and return a wrapped error that carries the
exit code (for example, create/use an ErrSubcommandFailedWithCode or include the
code in the error message/fields) so callers can decide process termination;
remove the os.Exit call and return fmt.Errorf("%w: exit status %d: %v",
errUtils.ErrSubcommandFailed, exitCode, err) or use the project's standard error
type that holds an exit code.
| var accessKeyID, secretAccessKey, mfaArn string | ||
| form := huh.NewForm( | ||
| huh.NewGroup( | ||
| huh.NewInput().Title("AWS Access Key ID").Value(&accessKeyID).Validate(func(s string) error { | ||
| if s == "" { | ||
| return fmt.Errorf("required") | ||
| } | ||
| return nil | ||
| }), | ||
| huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).Password(true).Validate(func(s string) error { | ||
| if s == "" { | ||
| return fmt.Errorf("required") | ||
| } | ||
| return nil | ||
| }), | ||
| huh.NewInput().Title("AWS User MFA ARN (optional)").Value(&mfaArn), | ||
| ), | ||
| ).WithTheme(uiutils.NewAtmosHuhTheme()) | ||
| if err := form.Run(); err != nil { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Use static-sentinel validation errors and fix deprecated Password API.
- Replace ad-hoc “required” with ErrInvalidArgumentError.
- Use EchoMode(EchoPassword) per huh deprecation.
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("AWS Access Key ID").Value(&accessKeyID).Validate(func(s string) error {
if s == "" {
- return fmt.Errorf("required")
+ return fmt.Errorf("%w: AWS Access Key ID is required", errUtils.ErrInvalidArgumentError)
}
return nil
}),
- huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).Password(true).Validate(func(s string) error {
+ huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).EchoMode(huh.EchoPassword).Validate(func(s string) error {
if s == "" {
- return fmt.Errorf("required")
+ return fmt.Errorf("%w: AWS Secret Access Key is required", errUtils.ErrInvalidArgumentError)
}
return nil
}),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| var accessKeyID, secretAccessKey, mfaArn string | |
| form := huh.NewForm( | |
| huh.NewGroup( | |
| huh.NewInput().Title("AWS Access Key ID").Value(&accessKeyID).Validate(func(s string) error { | |
| if s == "" { | |
| return fmt.Errorf("required") | |
| } | |
| return nil | |
| }), | |
| huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).Password(true).Validate(func(s string) error { | |
| if s == "" { | |
| return fmt.Errorf("required") | |
| } | |
| return nil | |
| }), | |
| huh.NewInput().Title("AWS User MFA ARN (optional)").Value(&mfaArn), | |
| ), | |
| ).WithTheme(uiutils.NewAtmosHuhTheme()) | |
| if err := form.Run(); err != nil { | |
| var accessKeyID, secretAccessKey, mfaArn string | |
| form := huh.NewForm( | |
| huh.NewGroup( | |
| huh.NewInput().Title("AWS Access Key ID").Value(&accessKeyID).Validate(func(s string) error { | |
| if s == "" { | |
| return fmt.Errorf("%w: AWS Access Key ID is required", errUtils.ErrInvalidArgumentError) | |
| } | |
| return nil | |
| }), | |
| huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).EchoMode(huh.EchoPassword).Validate(func(s string) error { | |
| if s == "" { | |
| return fmt.Errorf("%w: AWS Secret Access Key is required", errUtils.ErrInvalidArgumentError) | |
| } | |
| return nil | |
| }), | |
| huh.NewInput().Title("AWS User MFA ARN (optional)").Value(&mfaArn), | |
| ), | |
| ).WithTheme(uiutils.NewAtmosHuhTheme()) | |
| if err := form.Run(); err != nil { | |
| // ... | |
| } |
🧰 Tools
🪛 GitHub Check: golangci-lint
[failure] 76-76:
do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf("required")"
[failure] 80-80:
SA1019: huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).Password is deprecated: use EchoMode(EchoPassword) instead.
[failure] 82-82:
do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf("required")"
🤖 Prompt for AI Agents
In cmd/auth_user.go around lines 71 to 89, the form uses ad-hoc "required" error
strings and the deprecated Password(true) API; update the validators to return
the static sentinel error huh.ErrInvalidArgumentError instead of
fmt.Errorf("required") and replace .Password(true) with
.EchoMode(huh.EchoPassword) (or the equivalent huh.EchoMode constant) on the
secret input to use the new API.
| "github.com/spf13/viper" | ||
| ) | ||
|
|
||
| // authWhoamiCmd shows current authentication status |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Fix lint: end comment with a period.
Add a trailing period to satisfy godot.
-// authWhoamiCmd shows current authentication status
+// authWhoamiCmd shows current authentication status.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // authWhoamiCmd shows current authentication status | |
| // authWhoamiCmd shows current authentication status. |
🧰 Tools
🪛 GitHub Check: golangci-lint
[failure] 17-17:
Comment should end in a period
🤖 Prompt for AI Agents
In cmd/auth_whoami.go around line 17, the comment "authWhoamiCmd shows current
authentication status" is missing a trailing period; update the comment to end
with a period (e.g., "authWhoamiCmd shows current authentication status.") to
satisfy the linter.
| Use: "whoami", | ||
| Short: "Show current authentication status", | ||
| Long: "Display information about the current effective authentication principal.", | ||
|
|
||
| FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| return executeAuthWhoamiCommand(cmd, args) | ||
| }, | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Use function value for RunE (golangci-lint unlambda).
Assign the handler directly.
var authWhoamiCmd = &cobra.Command{
Use: "whoami",
Short: "Show current authentication status",
Long: "Display information about the current effective authentication principal.",
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
- RunE: func(cmd *cobra.Command, args []string) error {
- return executeAuthWhoamiCommand(cmd, args)
- },
+ RunE: executeAuthWhoamiCommand,
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Use: "whoami", | |
| Short: "Show current authentication status", | |
| Long: "Display information about the current effective authentication principal.", | |
| FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, | |
| RunE: func(cmd *cobra.Command, args []string) error { | |
| return executeAuthWhoamiCommand(cmd, args) | |
| }, | |
| } | |
| var authWhoamiCmd = &cobra.Command{ | |
| Use: "whoami", | |
| Short: "Show current authentication status", | |
| Long: "Display information about the current effective authentication principal.", | |
| FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, | |
| RunE: executeAuthWhoamiCommand, | |
| } |
🧰 Tools
🪛 GitHub Check: golangci-lint
[failure] 24-24:
unlambda: replace func(cmd *cobra.Command, args []string) error { return executeAuthWhoamiCommand(cmd, args) } with executeAuthWhoamiCommand
🤖 Prompt for AI Agents
In cmd/auth_whoami.go around lines 19 to 27, the RunE field is assigned an
anonymous wrapper that simply returns executeAuthWhoamiCommand; replace the
wrapper with the function value directly by setting RunE to
executeAuthWhoamiCommand so the handler is assigned without an extra lambda
(ensure the function signature matches func(*cobra.Command, []string) error).
| // Check if output should be JSON | ||
| outputFormat := viper.GetString("auth.whoami.output") | ||
| if outputFormat == "json" { | ||
| // Redact home directory in environment variable values before output | ||
| redactedWhoami := *whoami | ||
| homeDir, _ := homedir.Dir() | ||
| if whoami.Environment != nil && homeDir != "" { | ||
| redactedEnv := make(map[string]string, len(whoami.Environment)) | ||
| for k, v := range whoami.Environment { | ||
| redactedEnv[k] = redactHomeDir(v, homeDir) | ||
| } | ||
| redactedWhoami.Environment = redactedEnv | ||
| } | ||
| jsonData, err := json.MarshalIndent(redactedWhoami, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("failed to marshal JSON: %w", err) | ||
| } | ||
| fmt.Println(string(jsonData)) | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Do not emit credentials/sensitive env to stdout JSON (CodeQL).
WhoamiInfo may contain credentials; current JSON path only redacts homedir. Redact credentials by default and mask sensitive env keys. Add opt-in to include credentials if truly needed.
// Check if output should be JSON
outputFormat := viper.GetString("auth.whoami.output")
+if outputFormat != "" && outputFormat != "json" {
+ return fmt.Errorf("unsupported output format %q; supported: json", outputFormat)
+}
if outputFormat == "json" {
- // Redact home directory in environment variable values before output
- redactedWhoami := *whoami
- homeDir, _ := homedir.Dir()
- if whoami.Environment != nil && homeDir != "" {
- redactedEnv := make(map[string]string, len(whoami.Environment))
- for k, v := range whoami.Environment {
- redactedEnv[k] = redactHomeDir(v, homeDir)
- }
- redactedWhoami.Environment = redactedEnv
- }
- jsonData, err := json.MarshalIndent(redactedWhoami, "", " ")
+ // Sanitize output: redact credentials and sensitive env keys unless explicitly allowed.
+ includeCreds := viper.GetBool("auth.whoami.include_credentials")
+ homeDir, _ := homedir.Dir()
+ sanitized := *whoami
+ if !includeCreds {
+ // Drop credentials from output.
+ sanitized.Credentials = nil
+ }
+ if whoami.Environment != nil {
+ redactedEnv := make(map[string]string, len(whoami.Environment))
+ for k, v := range whoami.Environment {
+ val := redactHomeDir(v, homeDir)
+ if !includeCreds && isSensitiveEnvKey(k) {
+ redactedEnv[k] = maskSensitiveValue(val)
+ continue
+ }
+ redactedEnv[k] = val
+ }
+ sanitized.Environment = redactedEnv
+ }
+ jsonData, err := json.MarshalIndent(sanitized, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
fmt.Println(string(jsonData))
return nil
}Add helpers (outside the shown range):
// isSensitiveEnvKey returns true for keys that likely hold secrets/tokens.
func isSensitiveEnvKey(k string) bool {
uk := strings.ToUpper(k)
switch uk {
case "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_SECURITY_TOKEN", "AWS_ACCESS_KEY_ID":
return true
}
return strings.Contains(uk, "SECRET") || strings.Contains(uk, "TOKEN") || strings.Contains(uk, "PASSWORD")
}
// maskSensitiveValue replaces the middle portion of a secret with ellipsis.
func maskSensitiveValue(s string) string {
if len(s) <= 8 {
return "***REDACTED***"
}
return s[:4] + "...REDACTED..." + s[len(s)-4:]
}And bind an opt-in flag/env (see init() comment below).
🧰 Tools
🪛 GitHub Check: CodeQL
[failure] 75-75: Clear-text logging of sensitive information
Sensitive data returned by an access to passwdParts flows to a logging call.
🤖 Prompt for AI Agents
In cmd/auth_whoami.go around lines 58 to 77, the JSON branch currently only
redacts the home directory but may still emit sensitive credentials in
Environment; update the JSON serialization to mask sensitive env values by: add
helper functions isSensitiveEnvKey and maskSensitiveValue (as provided in the
review) elsewhere in the package, then when constructing redactedEnv check each
key with isSensitiveEnvKey and if true replace the value with
maskSensitiveValue(redactHomeDir(value, homeDir)), otherwise just
redactHomeDir(value, homeDir); additionally add an opt-in flag/env (e.g.,
auth.whoami.include-credentials) and only include unmasked original environment
when that flag is explicitly set to true.
| func init() { | ||
| authWhoamiCmd.Flags().StringP("output", "o", "", "Output format (json)") | ||
| _ = viper.BindPFlag("auth.whoami.output", authWhoamiCmd.Flags().Lookup("output")) | ||
| _ = viper.BindEnv("auth.whoami.output", "ATMOS_AUTH_WHOAMI_OUTPUT") | ||
| authCmd.AddCommand(authWhoamiCmd) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Bind an opt-in flag for credentials in JSON and (optionally) an identity override flag.
Keeps secure-by-default behavior while enabling advanced use.
func init() {
authWhoamiCmd.Flags().StringP("output", "o", "", "Output format (json)")
_ = viper.BindPFlag("auth.whoami.output", authWhoamiCmd.Flags().Lookup("output"))
_ = viper.BindEnv("auth.whoami.output", "ATMOS_AUTH_WHOAMI_OUTPUT")
+
+ // Security: require explicit opt-in to include credentials/sensitive env in JSON.
+ authWhoamiCmd.Flags().Bool("include-credentials", false, "Include credentials and sensitive environment values in JSON output (dangerous).")
+ _ = viper.BindPFlag("auth.whoami.include_credentials", authWhoamiCmd.Flags().Lookup("include-credentials"))
+ _ = viper.BindEnv("auth.whoami.include_credentials", "ATMOS_AUTH_WHOAMI_INCLUDE_CREDENTIALS")
+
+ // Optional: allow inspecting a specific identity instead of the default.
+ // authWhoamiCmd.Flags().String("identity", "", "Identity to inspect instead of the default.")
+ // _ = viper.BindPFlag("auth.whoami.identity", authWhoamiCmd.Flags().Lookup("identity"))
+ // _ = viper.BindEnv("auth.whoami.identity", "ATMOS_AUTH_WHOAMI_IDENTITY")
authCmd.AddCommand(authWhoamiCmd)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func init() { | |
| authWhoamiCmd.Flags().StringP("output", "o", "", "Output format (json)") | |
| _ = viper.BindPFlag("auth.whoami.output", authWhoamiCmd.Flags().Lookup("output")) | |
| _ = viper.BindEnv("auth.whoami.output", "ATMOS_AUTH_WHOAMI_OUTPUT") | |
| authCmd.AddCommand(authWhoamiCmd) | |
| } | |
| func init() { | |
| authWhoamiCmd.Flags().StringP("output", "o", "", "Output format (json)") | |
| _ = viper.BindPFlag("auth.whoami.output", authWhoamiCmd.Flags().Lookup("output")) | |
| _ = viper.BindEnv("auth.whoami.output", "ATMOS_AUTH_WHOAMI_OUTPUT") | |
| // Security: require explicit opt-in to include credentials/sensitive env in JSON. | |
| authWhoamiCmd.Flags().Bool("include-credentials", false, "Include credentials and sensitive environment values in JSON output (dangerous).") | |
| _ = viper.BindPFlag("auth.whoami.include_credentials", authWhoamiCmd.Flags().Lookup("include-credentials")) | |
| _ = viper.BindEnv("auth.whoami.include_credentials", "ATMOS_AUTH_WHOAMI_INCLUDE_CREDENTIALS") | |
| // Optional: allow inspecting a specific identity instead of the default. | |
| // authWhoamiCmd.Flags().String("identity", "", "Identity to inspect instead of the default.") | |
| // _ = viper.BindPFlag("auth.whoami.identity", authWhoamiCmd.Flags().Lookup("identity")) | |
| // _ = viper.BindEnv("auth.whoami.identity", "ATMOS_AUTH_WHOAMI_IDENTITY") | |
| authCmd.AddCommand(authWhoamiCmd) | |
| } |
🤖 Prompt for AI Agents
In cmd/auth_whoami.go around lines 115 to 120, the init function only registers
an output flag; add two additional opt-in flags --creds (string) for providing
credentials JSON and --identity (string) for an optional identity override, keep
both defaults empty to preserve secure-by-default behavior, bind them to viper
keys "auth.whoami.creds" and "auth.whoami.identity" and to env vars
"ATMOS_AUTH_WHOAMI_CREDS" and "ATMOS_AUTH_WHOAMI_IDENTITY", and ensure the flag
descriptions clearly state they are opt-in and that creds should be a JSON
string (do not log sensitive content).
| // TODO replace with Provider Interface Validate() | ||
| // Validate based on provider kind | ||
| switch provider.Kind { | ||
| case "aws/iam-identity-center": | ||
| return v.validateSSOProvider(provider) | ||
| case "aws/saml": | ||
| return v.validateSAMLProvider(provider) | ||
| case "github/oidc": | ||
| return v.validateGitHubOIDCProvider(provider) | ||
| default: | ||
| return fmt.Errorf("%w: unsupported provider kind: %s", errUtils.ErrInvalidProviderKind, provider.Kind) | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Use provider-scoped sentinel; add period to TODO.
- // TODO replace with Provider Interface Validate()
+ // TODO replace with Provider Interface Validate().
...
- default:
- return fmt.Errorf("%w: unsupported provider kind: %s", errUtils.ErrInvalidProviderKind, provider.Kind)
+ default:
+ return fmt.Errorf("%w: unsupported provider kind %q", errUtils.ErrInvalidProviderKind, provider.Kind)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // TODO replace with Provider Interface Validate() | |
| // Validate based on provider kind | |
| switch provider.Kind { | |
| case "aws/iam-identity-center": | |
| return v.validateSSOProvider(provider) | |
| case "aws/saml": | |
| return v.validateSAMLProvider(provider) | |
| case "github/oidc": | |
| return v.validateGitHubOIDCProvider(provider) | |
| default: | |
| return fmt.Errorf("%w: unsupported provider kind: %s", errUtils.ErrInvalidProviderKind, provider.Kind) | |
| } | |
| } | |
| // TODO replace with Provider Interface Validate(). | |
| // Validate based on provider kind | |
| switch provider.Kind { | |
| case "aws/iam-identity-center": | |
| return v.validateSSOProvider(provider) | |
| case "aws/saml": | |
| return v.validateSAMLProvider(provider) | |
| case "github/oidc": | |
| return v.validateGitHubOIDCProvider(provider) | |
| default: | |
| return fmt.Errorf("%w: unsupported provider kind %q", errUtils.ErrInvalidProviderKind, provider.Kind) | |
| } | |
| } |
🤖 Prompt for AI Agents
In internal/auth/validation/validator.go around lines 78 to 90, update the TODO
comment to end with a period and replace the cross-package sentinel
errUtils.ErrInvalidProviderKind with a provider-scoped sentinel (e.g., a
package-level ErrInvalidProviderKind defined in the auth/validation or the
provider package) so the validation function returns that local error; ensure
the new sentinel is exported if used across packages, update any callers/imports
accordingly, and keep the existing unsupported-kind error formatting when
returning the sentinel.
| // ValidateIdentity validates an identity configuration. | ||
| func (v *validator) ValidateIdentity(name string, identity *schema.Identity, providers map[string]*schema.Provider) error { | ||
| if name == "" { | ||
| return fmt.Errorf("%w: identity name cannot be empty", errUtils.ErrInvalidIdentityConfig) | ||
| } | ||
|
|
||
| if identity.Kind == "" { | ||
| return fmt.Errorf("%w: identity kind is required", errUtils.ErrInvalidIdentityConfig) | ||
| } | ||
|
|
||
| // Validate via configuration - AWS User identities don't require via provider | ||
| if identity.Kind != "aws/user" && identity.Via != nil { | ||
| if identity.Via.Provider != "" { | ||
| if _, exists := providers[identity.Via.Provider]; !exists { | ||
| return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // TODO replace with Identity Interface Validate() | ||
| // Validate based on identity kind | ||
| switch identity.Kind { | ||
| case "aws/permission-set": | ||
| return v.validatePermissionSetIdentity(identity) | ||
| case "aws/assume-role": | ||
| return v.validateAssumeRoleIdentity(identity) | ||
| case "aws/user": | ||
| return v.validateUserIdentity(identity) | ||
| default: | ||
| return fmt.Errorf("%w: unsupported identity kind: %s", errUtils.ErrInvalidAuthConfig, identity.Kind) | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Require via for non-user identities; use identity-scoped sentinels and proper default error.
- // Validate via configuration - AWS User identities don't require via provider
- if identity.Kind != "aws/user" && identity.Via != nil {
- if identity.Via.Provider != "" {
- if _, exists := providers[identity.Via.Provider]; !exists {
- return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider)
- }
- }
- }
+ // Validate via configuration - AWS User identities don't require via provider.
+ if identity.Kind != "aws/user" {
+ if identity.Via == nil {
+ return fmt.Errorf("%w: identity %q requires a via configuration", errUtils.ErrInvalidIdentityConfig, name)
+ }
+ // Exactly one of provider or identity must be set.
+ if (identity.Via.Provider == "" && identity.Via.Identity == "") || (identity.Via.Provider != "" && identity.Via.Identity != "") {
+ return fmt.Errorf("%w: identity %q must specify either via.provider or via.identity", errUtils.ErrInvalidIdentityConfig, name)
+ }
+ if identity.Via.Provider != "" {
+ if _, exists := providers[identity.Via.Provider]; !exists {
+ return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidIdentityConfig, identity.Via.Provider)
+ }
+ }
+ }
...
- default:
- return fmt.Errorf("%w: unsupported identity kind: %s", errUtils.ErrInvalidAuthConfig, identity.Kind)
+ default:
+ return fmt.Errorf("%w: unsupported identity kind %q", errUtils.ErrInvalidIdentityKind, identity.Kind)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // ValidateIdentity validates an identity configuration. | |
| func (v *validator) ValidateIdentity(name string, identity *schema.Identity, providers map[string]*schema.Provider) error { | |
| if name == "" { | |
| return fmt.Errorf("%w: identity name cannot be empty", errUtils.ErrInvalidIdentityConfig) | |
| } | |
| if identity.Kind == "" { | |
| return fmt.Errorf("%w: identity kind is required", errUtils.ErrInvalidIdentityConfig) | |
| } | |
| // Validate via configuration - AWS User identities don't require via provider | |
| if identity.Kind != "aws/user" && identity.Via != nil { | |
| if identity.Via.Provider != "" { | |
| if _, exists := providers[identity.Via.Provider]; !exists { | |
| return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider) | |
| } | |
| } | |
| } | |
| // TODO replace with Identity Interface Validate() | |
| // Validate based on identity kind | |
| switch identity.Kind { | |
| case "aws/permission-set": | |
| return v.validatePermissionSetIdentity(identity) | |
| case "aws/assume-role": | |
| return v.validateAssumeRoleIdentity(identity) | |
| case "aws/user": | |
| return v.validateUserIdentity(identity) | |
| default: | |
| return fmt.Errorf("%w: unsupported identity kind: %s", errUtils.ErrInvalidAuthConfig, identity.Kind) | |
| } | |
| } | |
| // ValidateIdentity validates an identity configuration. | |
| func (v *validator) ValidateIdentity(name string, identity *schema.Identity, providers map[string]*schema.Provider) error { | |
| if name == "" { | |
| return fmt.Errorf("%w: identity name cannot be empty", errUtils.ErrInvalidIdentityConfig) | |
| } | |
| if identity.Kind == "" { | |
| return fmt.Errorf("%w: identity kind is required", errUtils.ErrInvalidIdentityConfig) | |
| } | |
| // Validate via configuration - AWS User identities don't require via provider. | |
| if identity.Kind != "aws/user" { | |
| if identity.Via == nil { | |
| return fmt.Errorf("%w: identity %q requires a via configuration", errUtils.ErrInvalidIdentityConfig, name) | |
| } | |
| // Exactly one of provider or identity must be set. | |
| if (identity.Via.Provider == "" && identity.Via.Identity == "") || (identity.Via.Provider != "" && identity.Via.Identity != "") { | |
| return fmt.Errorf("%w: identity %q must specify either via.provider or via.identity", errUtils.ErrInvalidIdentityConfig, name) | |
| } | |
| if identity.Via.Provider != "" { | |
| if _, exists := providers[identity.Via.Provider]; !exists { | |
| return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidIdentityConfig, identity.Via.Provider) | |
| } | |
| } | |
| } | |
| // TODO replace with Identity Interface Validate() | |
| // Validate based on identity kind | |
| switch identity.Kind { | |
| case "aws/permission-set": | |
| return v.validatePermissionSetIdentity(identity) | |
| case "aws/assume-role": | |
| return v.validateAssumeRoleIdentity(identity) | |
| case "aws/user": | |
| return v.validateUserIdentity(identity) | |
| default: | |
| return fmt.Errorf("%w: unsupported identity kind %q", errUtils.ErrInvalidIdentityKind, identity.Kind) | |
| } | |
| } |
🤖 Prompt for AI Agents
internal/auth/validation/validator.go around lines 92 to 123: For non-"aws/user"
identity kinds require that identity.Via is non-nil (return
errUtils.ErrInvalidIdentityConfig with a clear message if missing), then
validate that identity.Via.Provider is present in providers and return
errUtils.ErrInvalidIdentityConfig (identity-scoped sentinel) if the referenced
provider does not exist; also switch the default/unrecognized-kind error to use
errUtils.ErrInvalidIdentityConfig instead of errUtils.ErrInvalidAuthConfig so
all identity validation failures use the identity-scoped sentinel.
| "auth": { | ||
| "logs": { | ||
| "file": "", | ||
| "level": "" | ||
| }, | ||
| "providers": null, | ||
| "identities": null | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Confirm JSON shape for providers/identities (null vs empty)
Downstream JSON consumers often prefer stable types (empty array/object) over null. If these are slices/maps, decide and document the contract now; otherwise you’ll add churn later.
Run to audit struct tags and uses:
🏁 Script executed:
#!/bin/bash
rg -nP -C2 'json:"(providers|identities)[^"]*"' --type go
rg -nP -C2 '\b(Providers|Identities)\b' --type go | rg -nP 'len\(|range |== nil'Length of output: 2718
Standardize JSON output for providers/identities in AuthConfig
Currently, AuthConfig in pkg/schema/schema.go (lines 977–978) omits omitempty on its json tags, so nil maps render as null in the CLI snapshot (tests/snapshots/TestCLICommands_atmos_describe_config.stdout.golden, lines 189–196). Downstream consumers often prefer empty objects ({}). In contrast, ComponentAuthConfig uses omitempty. Code handles nil maps safely via if … == nil and range loops. Decide on one contract now:
- To emit
{}, add,omitemptyto theAuthConfigtags or initialize these maps to empty before marshalling. - To allow
null, document the behavior and update tests/docs to expectnullconsistently.
🤖 Prompt for AI Agents
In pkg/schema/schema.go around lines 977-978, AuthConfig's providers and
identities are currently nil and render as null; to standardize CLI JSON output
to emit {} instead of null, ensure these map fields are initialized to empty
maps before marshalling (e.g., set them to map[string]T{} in the
constructor/loader or implement a MarshalJSON that replaces nil maps with empty
maps); make the change where configs are created/loaded so downstream code and
snapshots receive {} consistently.
There was a problem hiding this comment.
Actionable comments posted: 9
♻️ Duplicate comments (16)
cmd/auth_env.go (4)
38-42: Consistent static error wrapping.- // Create auth manager + // Create auth manager. authManager, err := createAuthManager(&atmosConfig.Auth) if err != nil { - return fmt.Errorf("failed to create auth manager: %w", err) + return fmt.Errorf("%w: failed to create auth manager: %v", errUtils.ErrStaticError, err) }
32-36: Wrap with static errors per policy.Don’t return dynamic errors directly; wrap with a static sentinel and include the dynamic error text.
- // Load atmos configuration + // Load atmos configuration. atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, true) if err != nil { - return fmt.Errorf("failed to load atmos config: %w", err) + return fmt.Errorf("%w: failed to load atmos config: %v", errUtils.ErrStaticError, err) }
55-59: Propagate command context; static error wrap.Use cmd.Context() for cancellation/telemetry; avoid returning the dynamic error directly.
- // Authenticate and get environment variables - ctx := context.Background() + // Authenticate and get environment variables. + ctx := cmd.Context() whoami, err := authManager.Authenticate(ctx, identityName) if err != nil { - return fmt.Errorf("authentication failed: %w", err) + return fmt.Errorf("%w: authentication failed: %v", errUtils.ErrStaticError, err) }Also remove the now-unused import of "context".
101-112: Bind the--identityflag to the persistent flag inauth_env.goReplace in
cmd/auth_env.go:- _ = viper.BindPFlag("identity", authEnvCmd.Flags().Lookup("identity")) + // Bind to the persistent --identity flag defined on authCmd. + _ = viper.BindPFlag("identity", authCmd.PersistentFlags().Lookup("identity"))This ensures the globally defined
--identityflag (inauthCmd) is correctly bound to Viper, restoring env var precedence and making--identityonauth envfunctional.cmd/auth_exec.go (3)
26-26: Remove FParseErrWhitelist when DisableFlagParsing is true.It’s ineffective and confusing with flag parsing disabled.
- FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
31-34: Avoid stack processing for exec.No stacks needed; set processStacks=false for faster startup.
- atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, true) + atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
23-25: Identity flag is ignored with DisableFlagParsing; implement manual pre---parsing + Viper/env precedence.With DisableFlagParsing, Cobra never parses
-i/--identity. Parse pre---args yourself, bind envATMOS_IDENTITY, and capture telemetry.- RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) (retErr error) { + // Capture telemetry for non-standard execution paths. + defer func() { telemetry.CaptureCmd(cmd, retErr) }() handleHelpRequest(cmd, args) checkAtmosConfig() - // Load atmos configuration + // Load atmos configuration. atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false) - if err != nil { - return fmt.Errorf("failed to load atmos config: %w", err) - } + if err != nil { + return fmt.Errorf("%w: failed to load atmos config: %v", errUtils.ErrSubcommandFailed, err) + } - // Create auth manager + // Create auth manager. authManager, err := createAuthManager(&atmosConfig.Auth) - if err != nil { - return fmt.Errorf("failed to create auth manager: %w", err) - } + if err != nil { + return fmt.Errorf("%w: failed to create auth manager: %v", errUtils.ErrSubcommandFailed, err) + } - // Get identity from flag or use default - identityName, _ := cmd.Flags().GetString("identity") + // Split args into our flags (pre `--`) and the delegated command args (post `--`). + ownArgs, delegatedArgs := splitExecArgs(args) + // Parse our minimal flagset manually since Cobra flag parsing is disabled. + fs := pflag.NewFlagSet("auth-exec", pflag.ContinueOnError) + var identityFlag string + fs.StringVarP(&identityFlag, "identity", "i", "", "Specify the identity to use for authentication.") + _ = fs.Parse(ownArgs) + // Bind env and respect precedence: flag > env > config/default. + _ = viper.BindEnv("identity", "ATMOS_IDENTITY") + identityName := identityFlag + if identityName == "" { + identityName = viper.GetString("identity") + } if identityName == "" { defaultIdentity, err := authManager.GetDefaultIdentity() if err != nil { - return fmt.Errorf("no default identity configured and no identity specified: %w", err) + return fmt.Errorf("%w: no default identity configured and no identity specified: %v", errUtils.ErrSubcommandFailed, err) } identityName = defaultIdentity } // Authenticate and get environment variables ctx := context.Background() whoami, err := authManager.Authenticate(ctx, identityName) if err != nil { - return fmt.Errorf("authentication failed: %w", err) + return fmt.Errorf("%w: authentication failed: %v", errUtils.ErrSubcommandFailed, err) } // Get environment variables from authentication result envVars := whoami.Environment if envVars == nil { envVars = make(map[string]string) } - // Execute the command with authentication environment - return executeCommandWithEnv(args, envVars) + // Execute the command with authentication environment. + return executeCommandWithEnv(delegatedArgs, envVars) },Add required imports:
@@ "syscall" errUtils "github.com/cloudposse/atmos/errors" cfg "github.com/cloudposse/atmos/pkg/config" "github.com/cloudposse/atmos/pkg/schema" + "github.com/cloudposse/atmos/pkg/telemetry" "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper"Bind env in init:
func init() { authExecCmd.Flags().StringP("identity", "i", "", "Specify the identity to use for authentication") + _ = viper.BindEnv("identity", "ATMOS_IDENTITY") authCmd.AddCommand(authExecCmd) }Helper outside the edited ranges:
// splitExecArgs splits our command flags (before `--`) from the delegated command args (after `--`). func splitExecArgs(args []string) (own []string, delegated []string) { for i, a := range args { if a == "--" { return args[:i], args[i+1:] } } return args, nil }Also applies to: 43-50
cmd/auth_user.go (2)
26-35: Wrap errors with sentinel and capture telemetry.Conform to error policy and add command telemetry.
-var authUserConfigureCmd = &cobra.Command{ +var authUserConfigureCmd = &cobra.Command{ Use: "configure", Short: "Configure static AWS user credentials (stored securely in keyring)", Long: `Configure static AWS user credentials (stored securely in keyring)`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) (retErr error) { + defer func() { telemetry.CaptureCmd(cmd, retErr) }() // Load atmos config atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false) if err != nil { - return err + return fmt.Errorf("%w: failed to load atmos config: %v", errUtils.ErrInvalidAuthConfig, err) }Add imports:
@@ "github.com/cloudposse/atmos/pkg/schema" + "github.com/cloudposse/atmos/pkg/telemetry"
39-41: Nil-safety for auth config.Guard atmosConfig.Auth before accessing Identities.
- if atmosConfig.Auth.Identities == nil { + if atmosConfig.Auth == nil || atmosConfig.Auth.Identities == nil { return fmt.Errorf("%w: no auth identities configured in atmos.yaml", errUtils.ErrInvalidAuthConfig) }tests/fixtures/scenarios/atmos-auth/atmos.yaml (1)
1-69: Ensure JSON Schemas expose auth.providers/identities.Confirm pkg/datafetcher/schema files define and validate
auth.providersandauth.identitiesso this fixture validates.#!/bin/bash set -euo pipefail echo "Search for 'auth' in JSON schemas:" rg -n '"auth"' pkg/datafetcher/schema || true echo echo "Search for 'providers' and 'identities' definitions:" rg -nP '"(providers|identities)"\s*:' pkg/datafetcher/schema || trueinternal/auth/validation/validator.go (6)
186-193: Use provider-scoped sentinel for GitHub OIDC.Consistent with provider validators.
func (v *validator) validateGitHubOIDCProvider(provider *schema.Provider) error { if provider.Region == "" { - return fmt.Errorf("%w: region is required for GitHub OIDC provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: region is required for GitHub OIDC provider", errUtils.ErrInvalidProviderConfig) } return nil }
153-164: Use provider-scoped sentinel for SSO validation.Aligns with error taxonomy.
func (v *validator) validateSSOProvider(provider *schema.Provider) error { if provider.StartURL == "" { - return fmt.Errorf("%w: start_url is required for AWS SSO provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: start_url is required for AWS SSO provider", errUtils.ErrInvalidProviderConfig) } if provider.Region == "" { - return fmt.Errorf("%w: region is required for AWS SSO provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: region is required for AWS SSO provider", errUtils.ErrInvalidProviderConfig) } return nil }
130-136: Validate referenced identities exist in chains.Avoids dangling edges and late failures.
for name, identity := range identities { if identity.Via != nil { if identity.Via.Identity != "" { + if _, ok := identities[identity.Via.Identity]; !ok { + return fmt.Errorf("%w: referenced identity %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Identity) + } graph[name] = append(graph[name], identity.Via.Identity) } } }
102-110: Require Via for non-user identities and validate shape.Prevents invalid chains at runtime.
- // Validate via configuration - AWS User identities don't require via provider - if identity.Kind != "aws/user" && identity.Via != nil { - if identity.Via.Provider != "" { - if _, exists := providers[identity.Via.Provider]; !exists { - return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider) - } - } - } + // Validate via configuration - AWS user identities don't require via provider. + if identity.Kind != "aws/user" { + if identity.Via == nil { + return fmt.Errorf("%w: identity %q requires a via configuration", errUtils.ErrInvalidAuthConfig, name) + } + // Exactly one of provider or identity must be set. + if (identity.Via.Provider == "" && identity.Via.Identity == "") || + (identity.Via.Provider != "" && identity.Via.Identity != "") { + return fmt.Errorf("%w: identity %q must specify either via.provider or via.identity", errUtils.ErrInvalidAuthConfig, name) + } + } + if identity.Via != nil && identity.Via.Provider != "" { + if _, exists := providers[identity.Via.Provider]; !exists { + return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider) + } + }
169-184: Harden SAML URL validation and use provider-scoped sentinel; fix multiple %w.ParseRequestURI and require scheme/host.
func (v *validator) validateSAMLProvider(provider *schema.Provider) error { if provider.URL == "" { - return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidProviderConfig) } if provider.Region == "" { - return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig) + return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidProviderConfig) } - // Validate URL format - if _, err := url.Parse(provider.URL); err != nil { - return fmt.Errorf("%w: invalid URL format: %w", errUtils.ErrInvalidAuthConfig, err) + // Validate URL format. + u, err := url.ParseRequestURI(provider.URL) + if err != nil || u.Scheme == "" || u.Host == "" { + return fmt.Errorf("%w: invalid URL format %q", errUtils.ErrInvalidProviderConfig, provider.URL) } return nil }
21-49: Validate logs, reuse converted maps, and fix multiple %w.Precompute provider/identity pointers once and avoid double-wrapping errors.
func (v *validator) ValidateAuthConfig(config *schema.AuthConfig) error { if config == nil { return fmt.Errorf("%w: auth config cannot be nil", errUtils.ErrInvalidAuthConfig) } - // Validate providers + // Validate logs. + if err := v.ValidateLogsConfig(&config.Logs); err != nil { + return fmt.Errorf("%w: logs configuration validation failed: %v", errUtils.ErrInvalidAuthConfig, err) + } + + // Precompute converted maps. + providersPtr := convertProviders(config.Providers) + identitiesPtr := convertIdentities(config.Identities) + + // Validate providers. //nolint:gocritic // rangeValCopy: map stores structs; address of map element can't be taken. Passing copy to factory is intended. for name, provider := range config.Providers { if err := v.ValidateProvider(name, &provider); err != nil { - return fmt.Errorf("%w: provider %q validation failed: %w", errUtils.ErrInvalidAuthConfig, name, err) + return fmt.Errorf("%w: provider %q validation failed: %v", errUtils.ErrInvalidAuthConfig, name, err) } } - // Validate identities + // Validate identities. for name, identity := range config.Identities { - if err := v.ValidateIdentity(name, &identity, convertProviders(config.Providers)); err != nil { - return fmt.Errorf("%w: identity %q validation failed: %w", errUtils.ErrInvalidAuthConfig, name, err) + if err := v.ValidateIdentity(name, &identity, providersPtr); err != nil { + return fmt.Errorf("%w: identity %q validation failed: %v", errUtils.ErrInvalidAuthConfig, name, err) } } - // Validate chains - if err := v.ValidateChains(convertIdentities(config.Identities), convertProviders(config.Providers)); err != nil { - return fmt.Errorf("%w: identity chain validation failed: %w", errUtils.ErrInvalidAuthConfig, err) + // Validate chains. + if err := v.ValidateChains(identitiesPtr, providersPtr); err != nil { + return fmt.Errorf("%w: identity chain validation failed: %v", errUtils.ErrInvalidAuthConfig, err) } return nil }
🧹 Nitpick comments (18)
tests/snapshots/TestCLICommands_atmos_auth_env_(CI_mode).stderr.golden (1)
2-5: Trim noisy error chain; add remediation hint.The repeated “authentication failed” wrappers make the message hard to scan. Consider surfacing a concise UI error (root cause + next step like: “Try: atmos auth login --identity ”) by default, and emit the full chain only with --verbose/-v.
tests/snapshots/TestCLICommands_atmos_describe_config.stdout.golden (1)
188-196: Avoid nulls in output; prefer omitempty.Emitting
"providers": nulland"identities": nulladds noise. Consideromitemptyon these fields (and empty log fields) so describe output stays lean.cmd/auth_env.go (4)
16-16: Document exported symbol.Add a short doc comment for SupportedFormats to satisfy docs/style expectations.
-var SupportedFormats = []string{"json", "export", "dotenv"} +// SupportedFormats lists valid output formats for `atmos auth env`. +var SupportedFormats = []string{"json", "export", "dotenv"}
18-23: Embed usage and examples.Per cmd guidelines, embed *_usage.md and show examples in help.
-import ( +import ( "context" "fmt" "slices" + _ "embed" errUtils "github.com/cloudposse/atmos/errors" @@ -// authEnvCmd exports authentication environment variables +//go:embed usage/auth_env_usage.md +var authEnvUsage string + +// authEnvCmd exports authentication environment variables. var authEnvCmd = &cobra.Command{ Use: "env", Short: "Export authentication environment variables", - Long: "Export environment variables for the authenticated identity to use with external tools.", + Long: "Export environment variables for the authenticated identity to use with external tools.", + Example: authEnvUsage,
80-80: Satisfy godot: end comments with periods.-// outputEnvAsJSON outputs environment variables as JSON +// outputEnvAsJSON outputs environment variables as JSON. @@ -// outputEnvAsExport outputs environment variables as shell export statements +// outputEnvAsExport outputs environment variables as shell export statements. @@ -// outputEnvAsDotenv outputs environment variables in .env format +// outputEnvAsDotenv outputs environment variables in .env format.Also applies to: 85-85, 93-93
85-99: Deterministic output; optional shell-safe quoting.Sort keys for stable snapshots. Optionally quote values to avoid shell/.env parsing surprises.
Stable order only:
func outputEnvAsExport(envVars map[string]string) error { - for key, value := range envVars { - fmt.Printf("export %s=\"%s\"\n", key, value) - } + keys := make([]string, 0, len(envVars)) + for k := range envVars { + keys = append(keys, k) + } + slices.Sort(keys) + for _, key := range keys { + value := envVars[key] + fmt.Printf("export %s=\"%s\"\n", key, value) + } return nil } @@ func outputEnvAsDotenv(envVars map[string]string) error { - for key, value := range envVars { - fmt.Printf("%s=%s\n", key, value) - } + keys := make([]string, 0, len(envVars)) + for k := range envVars { + keys = append(keys, k) + } + slices.Sort(keys) + for _, key := range keys { + value := envVars[key] + fmt.Printf("%s=%s\n", key, value) + } return nil }If you also want safe quoting, switch the format strings to use strconv.Quote(value) and import strconv.
cmd/auth_exec.go (2)
16-16: Add period to exported comment (godot).End the sentence with a period to satisfy golangci-lint.
-// authExecCmd executes a command with authentication environment variables +// authExecCmd executes a command with authentication environment variables.
70-71: Add period to exported comment (godot).-// executeCommandWithEnv executes a command with additional environment variables +// executeCommandWithEnv executes a command with additional environment variables.cmd/auth_user.go (2)
21-23: Polish help text (capitalization + period).- Short: "Manage aws user credentials for atmos auth", - Long: `Manage AWS user credentials for Atmos authentication.`, + Short: "Manage AWS user credentials for Atmos auth.", + Long: `Manage AWS user credentials for Atmos authentication.`,
56-63: Honor Identity.Default when preselecting.Use defaultChoice if available.
- // Choose identity - choice := selectable[0] + // Choose identity (prefer default if set). + choice := selectable[0] + if defaultChoice != "" { + choice = defaultChoice + } selector := huh.NewSelect[string](). Value(&choice). OptionsFunc(func() []huh.Option[string] { return huh.NewOptions(selectable...) }, nil). Title("Choose an identity to configure"). WithTheme(uiutils.NewAtmosHuhTheme())tests/fixtures/scenarios/atmos-auth/atmos.yaml (1)
10-10: Trim trailing spaces flagged by yamllint.Clean the trailing spaces on these blank lines to pass linters.
Also applies to: 16-16, 21-21, 39-39, 48-48, 55-55, 58-58
cmd/auth_whoami.go (4)
17-17: End the comment with a period.Lint requires sentence-ending periods for comments.
-// authWhoamiCmd shows current authentication status +// authWhoamiCmd shows current authentication status.
24-27: Avoid unnecessary function literal in RunE.Simplifies code and satisfies unlambda linter.
- RunE: func(cmd *cobra.Command, args []string) error { - return executeAuthWhoamiCommand(cmd, args) - }, + RunE: executeAuthWhoamiCommand,
3-15: Embed examples in help using //go:embed and Cobra Examples.Meets cmd/ guidelines; doesn’t change runtime behavior.
-import ( +import ( "context" "encoding/json" "fmt" + "embed" "os" "strings" @@ "github.com/spf13/cobra" "github.com/spf13/viper" ) + +//go:embed usage/auth_whoami_usage.md +var authWhoamiUsage string @@ var authWhoamiCmd = &cobra.Command{ Use: "whoami", Short: "Show current authentication status", Long: "Display information about the current effective authentication principal.", + Example: authWhoamiUsage,New file (create):
# cmd/usage/auth_whoami_usage.md atmos auth whoami atmos auth whoami -o json ATMOS_AUTH_WHOAMI_OUTPUT=json atmos auth whoamiAlso applies to: 18-22
46-55: Optional: capture telemetry on non-standard paths.Record "no default identity" and "no active session" outcomes.
I can wire telemetry.CaptureCmdString("auth/whoami", "no-default-identity") and ("no-active-session") if you want a patch.
internal/auth/validation/validator.go (3)
78-90: Provider default: consistent message and sentinel.Prefer quoted kind and a period.
- default: - return fmt.Errorf("%w: unsupported provider kind: %s", errUtils.ErrInvalidProviderKind, provider.Kind) + default: + return fmt.Errorf("%w: unsupported provider kind %q", errUtils.ErrInvalidProviderKind, provider.Kind)
121-122: Identity default: consistent message and sentinel.Prefer quoted kind and a period.
- default: - return fmt.Errorf("%w: unsupported identity kind: %s", errUtils.ErrInvalidAuthConfig, identity.Kind) + default: + return fmt.Errorf("%w: unsupported identity kind %q", errUtils.ErrInvalidAuthConfig, identity.Kind)
78-79: Finish TODO comments with periods.Keeps godot linter happy.
- // TODO replace with Provider Interface Validate() + // TODO replace with Provider Interface Validate(). @@ - // TODO replace with Identity Interface Validate() + // TODO replace with Identity Interface Validate().Also applies to: 111-113
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (21)
.github/workflows/test.yml(4 hunks)cmd/auth_env.go(1 hunks)cmd/auth_exec.go(1 hunks)cmd/auth_user.go(1 hunks)cmd/auth_whoami.go(1 hunks)errors/errors.go(1 hunks)examples/demo-auth/atmos.yaml(1 hunks)internal/auth/validation/validator.go(1 hunks)tests/fixtures/scenarios/atmos-auth/atmos.yaml(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_(CI_mode).stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--format_dotenv_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--format_export_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--format_invalid.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_env_--format_json_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_exec_--help.stdout.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_exec_--identity_test-user_--_echo_hello.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_login_--identity_test-user.stderr.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_merging.stdout.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_auth_user_configure_--help.stdout.golden(1 hunks)tests/snapshots/TestCLICommands_atmos_describe_config.stdout.golden(1 hunks)tests/test-cases/auth-cli.yaml(1 hunks)
✅ Files skipped from review due to trivial changes (4)
- tests/snapshots/TestCLICommands_atmos_auth_env_--format_json_--identity_test-user.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_exec_--help.stdout.golden
- tests/snapshots/TestCLICommands_atmos_auth_login_--identity_test-user.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_env_--format_dotenv_--identity_test-user.stderr.golden
🚧 Files skipped from review as they are similar to previous changes (8)
- errors/errors.go
- tests/snapshots/TestCLICommands_atmos_auth_exec_--identity_test-user_--_echo_hello.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_env_--format_invalid.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_env_--format_export_--identity_test-user.stderr.golden
- tests/snapshots/TestCLICommands_atmos_auth_user_configure_--help.stdout.golden
- tests/test-cases/auth-cli.yaml
- examples/demo-auth/atmos.yaml
- .github/workflows/test.yml
🧰 Additional context used
📓 Path-based instructions (4)
cmd/*.go
📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)
cmd/*.go: Implement each Cobra command in a separate file under the cmd/ directory
Use kebab-case for command-line flags
Provide comprehensive help text for all commands and flags
Include examples in command help
Provide meaningful feedback to users in command implementation
Include progress indicators for long-running operations
Provide clear error messages to users
Include troubleshooting hints when appropriate
Log detailed errors for debugging
Files:
cmd/auth_whoami.gocmd/auth_exec.gocmd/auth_env.gocmd/auth_user.go
**/*.go
📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)
**/*.go: Use Viper for managing configuration, environment variables, and flags
Use interfaces for external dependencies to facilitate mocking
All code must pass golangci-lint checks
Follow Go's error handling idioms
Use meaningful error messages
Wrap errors with context using fmt.Errorf("context: %w", err)
Consider using a custom error type for domain-specific errors
Follow standard Go coding style
Use gofmt and goimports to format code
Prefer short, descriptive variable names
Use snake_case for environment variables
Document all exported functions, types, and methods
Document complex logic with inline comments
Follow Go's documentation conventions
Use Viper for configuration management
Support configuration via files, environment variables, and flags
Follow the precedence order: flags > environment variables > config file > defaults
**/*.go: All comments in Go code must end with periods (enforced by golangci-lint godot).
Wrap all returned errors using static errors from the errors package; never return dynamic errors directly.
Always bind environment variables with viper.BindEnv() and provide ATMOS_ alternatives for each external var.
Separate structured logging from UI output: use stderr for prompts/errors to user; stdout only for data; never use logging for UI.
Most text UI must go to stderr; only data/results to stdout. Prefer utils.PrintfMessageToTUI for UI messages.
For non-standard execution paths, capture telemetry with telemetry.CaptureCmd or telemetry.CaptureCmdString and never capture user data.
Ensure cross-platform compatibility: prefer SDKs to external binaries, use filepath/os/runtime for portability, and add build constraints when needed.
Files:
cmd/auth_whoami.gocmd/auth_exec.gocmd/auth_env.gointernal/auth/validation/validator.gocmd/auth_user.go
cmd/**/!(*_test).go
📄 CodeRabbit inference engine (CLAUDE.md)
cmd/**/!(*_test).go: One Cobra command per file in cmd/.
Use //go:embed to load *_usage.md examples and utils.PrintfMarkdown() to render them in commands.
Files:
cmd/auth_whoami.gocmd/auth_exec.gocmd/auth_env.gocmd/auth_user.go
**/*.{yaml,yml}
📄 CodeRabbit inference engine (CLAUDE.md)
YAML configs should support Go templating using provided template functions.
Files:
tests/fixtures/scenarios/atmos-auth/atmos.yaml
🧠 Learnings (1)
📚 Learning: 2025-09-09T02:14:36.682Z
Learnt from: Benbentwo
PR: cloudposse/atmos#1452
File: internal/auth/types/whoami.go:14-15
Timestamp: 2025-09-09T02:14:36.682Z
Learning: The WhoamiInfo struct in internal/auth/types/whoami.go requires the Credentials field to be JSON-serializable for keystore unmarshaling operations, despite security concerns about credential exposure.
Applied to files:
cmd/auth_whoami.go
🧬 Code graph analysis (5)
cmd/auth_whoami.go (4)
pkg/schema/schema.go (4)
Command(645-655)ConfigAndStacksInfo(460-539)Provider(982-994)Identity(1002-1010)pkg/config/config.go (1)
InitCliConfig(25-62)pkg/config/go-homedir/homedir.go (1)
Dir(36-63)internal/auth/types/interfaces.go (2)
Provider(53-72)Identity(75-95)
cmd/auth_exec.go (3)
pkg/config/config.go (1)
InitCliConfig(25-62)pkg/schema/schema.go (1)
ConfigAndStacksInfo(460-539)errors/errors.go (2)
ErrInvalidSubcommand(96-96)ErrSubcommandFailed(97-97)
cmd/auth_env.go (4)
pkg/schema/schema.go (3)
Command(645-655)ConfigAndStacksInfo(460-539)AtmosConfiguration(25-61)errors/errors.go (1)
ErrInvalidArgumentError(99-99)pkg/config/config.go (1)
InitCliConfig(25-62)pkg/utils/json_utils.go (1)
PrintAsJSON(16-23)
internal/auth/validation/validator.go (3)
internal/auth/types/interfaces.go (3)
Validator(157-169)Provider(53-72)Identity(75-95)pkg/schema/schema.go (4)
AuthConfig(975-979)Logs(366-369)Provider(982-994)Identity(1002-1010)errors/errors.go (4)
ErrInvalidAuthConfig(83-83)ErrInvalidProviderConfig(87-87)ErrInvalidProviderKind(86-86)ErrInvalidIdentityConfig(85-85)
cmd/auth_user.go (6)
pkg/schema/schema.go (2)
Command(645-655)ConfigAndStacksInfo(460-539)pkg/config/config.go (1)
InitCliConfig(25-62)errors/errors.go (1)
ErrInvalidAuthConfig(83-83)internal/tui/utils/utils.go (1)
NewAtmosHuhTheme(85-93)internal/auth/credentials/store.go (1)
NewCredentialStore(29-31)internal/auth/types/aws_credentials.go (1)
AWSCredentials(6-13)
🪛 GitHub Check: golangci-lint
cmd/auth_whoami.go
[failure] 17-17:
Comment should end in a period
[failure] 24-24:
unlambda: replace func(cmd *cobra.Command, args []string) error { return executeAuthWhoamiCommand(cmd, args) } with executeAuthWhoamiCommand
cmd/auth_exec.go
[failure] 16-16:
Comment should end in a period
[failure] 70-70:
Comment should end in a period
[failure] 105-105:
deep-exit: calls to os.Exit only in main() or init() functions
cmd/auth_env.go
[failure] 18-18:
Comment should end in a period
[failure] 80-80:
Comment should end in a period
[failure] 85-85:
Comment should end in a period
[failure] 93-93:
Comment should end in a period
cmd/auth_user.go
[failure] 76-76:
do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf("required")"
[failure] 80-80:
SA1019: huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).Password is deprecated: use EchoMode(EchoPassword) instead.
[failure] 82-82:
do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf("required")"
🪛 GitHub Check: CodeQL
cmd/auth_whoami.go
[failure] 75-75: Clear-text logging of sensitive information
Sensitive data returned by an access to passwdParts flows to a logging call.
🪛 YAMLlint (1.37.1)
tests/fixtures/scenarios/atmos-auth/atmos.yaml
[error] 10-10: trailing spaces
(trailing-spaces)
[error] 16-16: trailing spaces
(trailing-spaces)
[error] 21-21: trailing spaces
(trailing-spaces)
[error] 39-39: trailing spaces
(trailing-spaces)
[error] 48-48: trailing spaces
(trailing-spaces)
[error] 55-55: trailing spaces
(trailing-spaces)
[error] 58-58: trailing spaces
(trailing-spaces)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Acceptance Tests (macos-latest, macos)
- GitHub Check: Acceptance Tests (windows-latest, windows)
- GitHub Check: Acceptance Tests (ubuntu-latest, linux)
- GitHub Check: Summary
🔇 Additional comments (2)
internal/auth/validation/validator.go (1)
3-11: Add regexp import (used for ARN validation).Required for robust ARN checks below.
import ( "fmt" "net/url" + "regexp" "strings"Likely an incorrect or invalid review comment.
tests/snapshots/TestCLICommands_atmos_auth_merging.stdout.golden (1)
1-54: Snapshot looks good.Values are placeholders; no secrets leaked. LGTM.
| // Get output format | ||
| format, _ := cmd.Flags().GetString("format") | ||
| if !slices.Contains(SupportedFormats, format) { | ||
| return fmt.Errorf("%w invalid format: %s", errUtils.ErrInvalidArgumentError, format) | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Honor config precedence; read format from Viper; fix error formatting.
Use viper (flags > env > config). Also add the missing colon after %w.
- // Get output format
- format, _ := cmd.Flags().GetString("format")
- if !slices.Contains(SupportedFormats, format) {
- return fmt.Errorf("%w invalid format: %s", errUtils.ErrInvalidArgumentError, format)
- }
+ // Get output format from Viper (flags > env > config).
+ format := viper.GetString("auth_env_format")
+ if format == "" {
+ format = "export"
+ }
+ if !slices.Contains(SupportedFormats, format) {
+ return fmt.Errorf("%w: invalid format: %s", errUtils.ErrInvalidArgumentError, format)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Get output format | |
| format, _ := cmd.Flags().GetString("format") | |
| if !slices.Contains(SupportedFormats, format) { | |
| return fmt.Errorf("%w invalid format: %s", errUtils.ErrInvalidArgumentError, format) | |
| } | |
| // Get output format from Viper (flags > env > config). | |
| format := viper.GetString("auth_env_format") | |
| if format == "" { | |
| format = "export" | |
| } | |
| if !slices.Contains(SupportedFormats, format) { | |
| return fmt.Errorf("%w: invalid format: %s", errUtils.ErrInvalidArgumentError, format) | |
| } |
🤖 Prompt for AI Agents
In cmd/auth_env.go around lines 26–31, the code reads the format directly from
the flag and constructs the error without the required colon; change to read the
format via viper (so precedence honors flags > env > config—ensure the flag has
been bound to viper elsewhere) by replacing the direct cmd.Flags().GetString
call with viper.GetString("format"), validate against SupportedFormats as
before, and fix the error formatting to include the colon by using
fmt.Errorf("%w: invalid format: %s", errUtils.ErrInvalidArgumentError, format).
| // Get identity from flag or use default | ||
| identityName, _ := cmd.Flags().GetString("identity") | ||
| if identityName == "" { | ||
| defaultIdentity, err := authManager.GetDefaultIdentity() | ||
| if err != nil { | ||
| return fmt.Errorf("no default identity configured and no identity specified: %w", err) | ||
| } | ||
| identityName = defaultIdentity | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Read identity from Viper; fix precedence and error wrapping.
This ensures env overrides (ATMOS_IDENTITY) work and aligns with policy.
- // Get identity from flag or use default
- identityName, _ := cmd.Flags().GetString("identity")
+ // Get identity from Viper (flags > env > config) or use default.
+ identityName := viper.GetString("identity")
if identityName == "" {
defaultIdentity, err := authManager.GetDefaultIdentity()
if err != nil {
- return fmt.Errorf("no default identity configured and no identity specified: %w", err)
+ return fmt.Errorf("%w: no default identity configured and no identity specified: %v", errUtils.ErrStaticError, err)
}
identityName = defaultIdentity
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Get identity from flag or use default | |
| identityName, _ := cmd.Flags().GetString("identity") | |
| if identityName == "" { | |
| defaultIdentity, err := authManager.GetDefaultIdentity() | |
| if err != nil { | |
| return fmt.Errorf("no default identity configured and no identity specified: %w", err) | |
| } | |
| identityName = defaultIdentity | |
| } | |
| // Get identity from Viper (flags > env > config) or use default. | |
| identityName := viper.GetString("identity") | |
| if identityName == "" { | |
| defaultIdentity, err := authManager.GetDefaultIdentity() | |
| if err != nil { | |
| return fmt.Errorf("%w: no default identity configured and no identity specified: %v", errUtils.ErrStaticError, err) | |
| } | |
| identityName = defaultIdentity | |
| } |
🤖 Prompt for AI Agents
In cmd/auth_env.go around lines 44-52, change identity resolution to read from
Viper first (so ATMOS_IDENTITY env var overrides), then fall back to the CLI
flag, and finally call authManager.GetDefaultIdentity(); specifically use
viper.GetString("identity") and only call cmd.Flags().GetString("identity") if
viper returns empty, handling and returning any error from GetString properly,
and if both are empty call GetDefaultIdentity and wrap its error with
fmt.Errorf("no default identity configured and no identity specified: %w", err).
| err = execCmd.Run() | ||
| if err != nil { | ||
| // If it's an exit error, preserve the exit code | ||
| if exitError, ok := err.(*exec.ExitError); ok { | ||
| if status, ok := exitError.Sys().(syscall.WaitStatus); ok { | ||
| os.Exit(status.ExitStatus()) | ||
| } | ||
| } | ||
| return fmt.Errorf("%w: command execution failed: %v", errUtils.ErrSubcommandFailed, err) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Don’t call os.Exit here (deep-exit); propagate ExitError.
Return the underlying ExitError so the root can set exit codes; also wrap other errors with a sentinel.
- err = execCmd.Run()
+ err = execCmd.Run()
if err != nil {
- // If it's an exit error, preserve the exit code
+ // If it's an exit error, propagate it to the caller.
if exitError, ok := err.(*exec.ExitError); ok {
- if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
- os.Exit(status.ExitStatus())
- }
+ return exitError
}
- return fmt.Errorf("%w: command execution failed: %v", errUtils.ErrSubcommandFailed, err)
+ return fmt.Errorf("%w: command execution failed: %v", errUtils.ErrSubcommandFailed, err)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| err = execCmd.Run() | |
| if err != nil { | |
| // If it's an exit error, preserve the exit code | |
| if exitError, ok := err.(*exec.ExitError); ok { | |
| if status, ok := exitError.Sys().(syscall.WaitStatus); ok { | |
| os.Exit(status.ExitStatus()) | |
| } | |
| } | |
| return fmt.Errorf("%w: command execution failed: %v", errUtils.ErrSubcommandFailed, err) | |
| } | |
| err = execCmd.Run() | |
| if err != nil { | |
| // If it's an exit error, propagate it to the caller. | |
| if exitError, ok := err.(*exec.ExitError); ok { | |
| return exitError | |
| } | |
| return fmt.Errorf("%w: command execution failed: %v", errUtils.ErrSubcommandFailed, err) | |
| } |
🧰 Tools
🪛 GitHub Check: golangci-lint
[failure] 105-105:
deep-exit: calls to os.Exit only in main() or init() functions
🤖 Prompt for AI Agents
In cmd/auth_exec.go around lines 100 to 109, remove the os.Exit call and instead
propagate the ExitError to the caller so the root can set the process exit code;
specifically, if err is an *exec.ExitError return that ExitError directly (do
not call os.Exit or wrap it), and for non-exit errors return them wrapped with
the sentinel errUtils.ErrSubcommandFailed (e.g. fmt.Errorf("%w: command
execution failed: %v", errUtils.ErrSubcommandFailed, err)).
| huh.NewInput().Title("AWS Access Key ID").Value(&accessKeyID).Validate(func(s string) error { | ||
| if s == "" { | ||
| return fmt.Errorf("required") | ||
| } | ||
| return nil | ||
| }), | ||
| huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).Password(true).Validate(func(s string) error { | ||
| if s == "" { | ||
| return fmt.Errorf("required") | ||
| } | ||
| return nil | ||
| }), | ||
| huh.NewInput().Title("AWS User MFA ARN (optional)").Value(&mfaArn), |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Fix deprecated Password() and wrap validation errors with sentinel.
Use EchoMode(EchoPassword) and static error wrapping.
- huh.NewInput().Title("AWS Access Key ID").Value(&accessKeyID).Validate(func(s string) error {
+ huh.NewInput().Title("AWS Access Key ID").Value(&accessKeyID).Validate(func(s string) error {
if s == "" {
- return fmt.Errorf("required")
+ return fmt.Errorf("%w: required", errUtils.ErrInvalidAuthConfig)
}
return nil
}),
- huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).Password(true).Validate(func(s string) error {
+ huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).EchoMode(huh.EchoPassword).Validate(func(s string) error {
if s == "" {
- return fmt.Errorf("required")
+ return fmt.Errorf("%w: required", errUtils.ErrInvalidAuthConfig)
}
return nil
}),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| huh.NewInput().Title("AWS Access Key ID").Value(&accessKeyID).Validate(func(s string) error { | |
| if s == "" { | |
| return fmt.Errorf("required") | |
| } | |
| return nil | |
| }), | |
| huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).Password(true).Validate(func(s string) error { | |
| if s == "" { | |
| return fmt.Errorf("required") | |
| } | |
| return nil | |
| }), | |
| huh.NewInput().Title("AWS User MFA ARN (optional)").Value(&mfaArn), | |
| huh.NewInput().Title("AWS Access Key ID").Value(&accessKeyID).Validate(func(s string) error { | |
| if s == "" { | |
| return fmt.Errorf("%w: required", errUtils.ErrInvalidAuthConfig) | |
| } | |
| return nil | |
| }), | |
| huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).EchoMode(huh.EchoPassword).Validate(func(s string) error { | |
| if s == "" { | |
| return fmt.Errorf("%w: required", errUtils.ErrInvalidAuthConfig) | |
| } | |
| return nil | |
| }), | |
| huh.NewInput().Title("AWS User MFA ARN (optional)").Value(&mfaArn), |
🧰 Tools
🪛 GitHub Check: golangci-lint
[failure] 76-76:
do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf("required")"
[failure] 80-80:
SA1019: huh.NewInput().Title("AWS Secret Access Key").Value(&secretAccessKey).Password is deprecated: use EchoMode(EchoPassword) instead.
[failure] 82-82:
do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf("required")"
🤖 Prompt for AI Agents
In cmd/auth_user.go around lines 74 to 86, the input creation uses the
deprecated Password() and returns plain error strings; replace Password() with
EchoMode(huh.EchoPassword) and change validation to return a package-level
sentinel error (e.g. ErrRequired) wrapped statically using fmt.Errorf("%w",
ErrRequired) instead of fmt.Errorf("required"); add the sentinel error
definition in this file (var ErrRequired = errors.New("required")) and ensure
the Validate callbacks return fmt.Errorf("%w", ErrRequired) when the value is
empty.
| if err := form.Run(); err != nil { | ||
| return err | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Wrap form error with sentinel.
- if err := form.Run(); err != nil {
- return err
- }
+ if err := form.Run(); err != nil {
+ return fmt.Errorf("%w: form submission failed: %v", errUtils.ErrInvalidAuthConfig, err)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if err := form.Run(); err != nil { | |
| return err | |
| } | |
| if err := form.Run(); err != nil { | |
| return fmt.Errorf("%w: form submission failed: %v", errUtils.ErrInvalidAuthConfig, err) | |
| } |
🤖 Prompt for AI Agents
In cmd/auth_user.go around lines 89 to 91, the returned error from form.Run()
must be wrapped with a sentinel error so callers can detect this failure;
replace "return err" with a wrapped error using a package-level sentinel (e.g.,
ErrFormInvalid) like: if err := form.Run(); err != nil { return fmt.Errorf("%w:
%v", ErrFormInvalid, err) } and if the sentinel doesn't exist, declare var
ErrFormInvalid = errors.New("form invalid") in the package.
| if err := store.Store(alias, creds); err != nil { | ||
| return err | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Wrap credential store error with sentinel.
- if err := store.Store(alias, creds); err != nil {
- return err
- }
+ if err := store.Store(alias, creds); err != nil {
+ return fmt.Errorf("%w: failed to store credentials: %v", errUtils.ErrInvalidAuthConfig, err)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if err := store.Store(alias, creds); err != nil { | |
| return err | |
| } | |
| if err := store.Store(alias, creds); err != nil { | |
| return fmt.Errorf("%w: failed to store credentials: %v", errUtils.ErrInvalidAuthConfig, err) | |
| } |
🤖 Prompt for AI Agents
In cmd/auth_user.go around lines 104–106, the error returned from store.Store
should be wrapped with a sentinel error so callers can use errors.Is; replace
the direct return with a wrapped error like: return fmt.Errorf("%w: %v",
ErrCredentialStore, err) (ensure ErrCredentialStore is defined/ exported and
import fmt), so the sentinel is the wrapped error while preserving the original
error text for debugging.
| atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to load atmos config: %w", err) | ||
| } | ||
|
|
||
| // Create auth manager | ||
| authManager, err := createAuthManager(&atmosConfig.Auth) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create auth manager: %w", err) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Wrap errors with a static sentinel; use only one %w per fmt.Errorf.
Conforms to repo error policy and go vet.
- atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
+ atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
if err != nil {
- return fmt.Errorf("failed to load atmos config: %w", err)
+ return fmt.Errorf("%w: failed to load atmos config: %v", errUtils.ErrStaticError, err)
}
@@
- authManager, err := createAuthManager(&atmosConfig.Auth)
+ authManager, err := createAuthManager(&atmosConfig.Auth)
if err != nil {
- return fmt.Errorf("failed to create auth manager: %w", err)
+ return fmt.Errorf("%w: failed to create auth manager: %v", errUtils.ErrStaticError, err)
}
@@
- jsonData, err := json.MarshalIndent(redactedWhoami, "", " ")
+ jsonData, err := json.MarshalIndent(redactedWhoami, "", " ")
if err != nil {
- return fmt.Errorf("failed to marshal JSON: %w", err)
+ return fmt.Errorf("%w: failed to marshal JSON: %v", errUtils.ErrStaticError, err)
}Add import:
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
@@
"github.com/spf13/cobra"
"github.com/spf13/viper"
+ errUtils "github.com/cloudposse/atmos/errors"
)Also applies to: 71-74
🤖 Prompt for AI Agents
In cmd/auth_whoami.go around lines 31-40 (and also apply same change to 71-74),
replace the current fmt.Errorf wrapping with a static sentinel error: add a
package-level sentinel var (e.g., var ErrLoadAtmosConfig = errors.New("failed to
load atmos config")) and import "errors", then return errors using
fmt.Errorf("%w: %v", ErrLoadAtmosConfig, err) so only one %w is used (the
sentinel) and the original error is included with %v; repeat the same pattern
for the auth manager creation error (create a distinct sentinel like
ErrCreateAuthManager and return fmt.Errorf("%w: %v", ErrCreateAuthManager,
err)).
| // Check if output should be JSON | ||
| outputFormat := viper.GetString("auth.whoami.output") | ||
| if outputFormat == "json" { | ||
| // Redact home directory in environment variable values before output | ||
| redactedWhoami := *whoami | ||
| homeDir, _ := homedir.Dir() | ||
| if whoami.Environment != nil && homeDir != "" { | ||
| redactedEnv := make(map[string]string, len(whoami.Environment)) | ||
| for k, v := range whoami.Environment { | ||
| redactedEnv[k] = redactHomeDir(v, homeDir) | ||
| } | ||
| redactedWhoami.Environment = redactedEnv | ||
| } | ||
| jsonData, err := json.MarshalIndent(redactedWhoami, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("failed to marshal JSON: %w", err) | ||
| } | ||
| fmt.Println(string(jsonData)) | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Redact secrets in JSON output (CodeQL: clear-text credentials).
Whoami JSON can leak AccessKey/Secret/Token. Keep JSON-serializable structure but mask sensitive fields.
// Check if output should be JSON
outputFormat := viper.GetString("auth.whoami.output")
if outputFormat == "json" {
// Redact home directory in environment variable values before output
redactedWhoami := *whoami
homeDir, _ := homedir.Dir()
if whoami.Environment != nil && homeDir != "" {
redactedEnv := make(map[string]string, len(whoami.Environment))
for k, v := range whoami.Environment {
redactedEnv[k] = redactHomeDir(v, homeDir)
}
redactedWhoami.Environment = redactedEnv
}
+ // Redact credentials (mask well-known secret keys while preserving JSON shape).
+ if whoami.Credentials != nil {
+ redactedWhoami.Credentials = redactCredentials(whoami.Credentials)
+ }
jsonData, err := json.MarshalIndent(redactedWhoami, "", " ")
if err != nil {
return fmt.Errorf("%w: failed to marshal JSON: %v", errUtils.ErrStaticError, err)
}
fmt.Println(string(jsonData))
return nil
}Add helpers below redactHomeDir:
func redactHomeDir(v string, homeDir string) string {
@@
return v
}
+// redactCredentials masks common secret fields in a credentials payload while retaining its JSON-serializable shape.
+func redactCredentials(creds interface{}) interface{} {
+ switch c := creds.(type) {
+ case map[string]string:
+ out := make(map[string]string, len(c))
+ for k, v := range c {
+ if isSecretKey(k) {
+ out[k] = maskSecret(v)
+ } else {
+ out[k] = v
+ }
+ }
+ return out
+ case map[string]interface{}:
+ out := make(map[string]interface{}, len(c))
+ for k, v := range c {
+ if isSecretKey(k) {
+ if s, ok := v.(string); ok {
+ out[k] = maskSecret(s)
+ continue
+ }
+ }
+ out[k] = v
+ }
+ return out
+ default:
+ // Fallback: best-effort redact via JSON round-trip.
+ var m map[string]interface{}
+ b, err := json.Marshal(creds)
+ if err != nil {
+ return creds
+ }
+ if err := json.Unmarshal(b, &m); err != nil {
+ return creds
+ }
+ return redactCredentials(m)
+ }
+}
+
+func isSecretKey(k string) bool {
+ s := strings.ToLower(k)
+ return strings.Contains(s, "secret") ||
+ strings.Contains(s, "password") ||
+ strings.Contains(s, "token") ||
+ s == "access_key_id" || s == "secret_access_key" || s == "session_token" ||
+ s == "client_secret" || s == "id_token"
+}
+
+func maskSecret(v string) string {
+ if v == "" {
+ return ""
+ }
+ if len(v) <= 4 {
+ return strings.Repeat("*", len(v))
+ }
+ return v[:4] + strings.Repeat("*", len(v)-4)
+}Note: This preserves serializability required by internal/auth/types/whoami.go while preventing credential disclosure.
🧰 Tools
🪛 GitHub Check: CodeQL
[failure] 75-75: Clear-text logging of sensitive information
Sensitive data returned by an access to passwdParts flows to a logging call.
🤖 Prompt for AI Agents
In cmd/auth_whoami.go around lines 58 to 77, the JSON branch currently only
redacts home directory paths but may still serialize clear-text credentials; add
a masking step before marshaling to remove secrets. Implement a small helper
(e.g., maskSensitiveWhoami) placed below redactHomeDir that takes the whoami
struct (or its redacted copy), replaces sensitive fields (AccessKey, Secret,
Token and any other credential fields defined in internal/auth/types/whoami.go)
with a constant placeholder like "<redacted>" when non-empty, and returns the
masked struct; call this helper after you build redactedWhoami and before
json.MarshalIndent. Ensure the helper preserves the JSON-serializable shape (no
type changes) and does not modify other fields (environment already has home-dir
redaction).
| // validateAssumeRoleIdentity validates AWS assume role identity configuration. | ||
| func (v *validator) validateAssumeRoleIdentity(identity *schema.Identity) error { | ||
| if identity.Principal == nil { | ||
| return fmt.Errorf("%w: principal is required for assume role identity", errUtils.ErrInvalidAuthConfig) | ||
| } | ||
|
|
||
| roleArn, ok := identity.Principal["assume_role"].(string) | ||
| if !ok || roleArn == "" { | ||
| return fmt.Errorf("%w: assume_role is required in principal", errUtils.ErrInvalidAuthConfig) | ||
| } | ||
|
|
||
| if !strings.HasPrefix(roleArn, "arn:aws:iam::") { | ||
| return fmt.Errorf("%w: invalid role ARN format", errUtils.ErrInvalidAuthConfig) | ||
| } | ||
|
|
||
| return nil | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Accept all AWS partitions for role ARN; provide actionable error.
Loosen to arn:[partition]:iam::ACCOUNT_ID:role/NAME and give context.
func (v *validator) validateAssumeRoleIdentity(identity *schema.Identity) error {
@@
- if !strings.HasPrefix(roleArn, "arn:aws:iam::") {
- return fmt.Errorf("%w: invalid role ARN format", errUtils.ErrInvalidAuthConfig)
- }
+ // Accept aws, aws-us-gov, aws-cn, etc.
+ re := regexp.MustCompile(`^arn:[^:]+:iam::\d{12}:role\/.+$`)
+ if !re.MatchString(roleArn) {
+ return fmt.Errorf("%w: invalid role ARN format %q", errUtils.ErrInvalidAuthConfig, roleArn)
+ }
return nil
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
internal/auth/validation/validator.go around lines 220 to 236: current
validation restricts ARNs to the "aws" partition only; update the check to
accept ARNs for any AWS partition using a pattern like
"arn:[^:]+:iam::ACCOUNT_ID:role/NAME" (e.g., validate with a regex matching
/^arn:[^:]+:iam::\d{12}:role\/.+$/) and replace the generic error with a
clearer, actionable message (e.g., include the invalid roleArn and state the
expected ARN pattern) so callers know what format to provide.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (4)
internal/auth/validation/validator.go (4)
159-167: Provider validators should return provider-scoped sentinels.- if provider.StartURL == "" { - return fmt.Errorf("%w: start_url is required for AWS SSO provider", errUtils.ErrInvalidAuthConfig) - } + if provider.StartURL == "" { + return fmt.Errorf("%w: start_url is required for AWS SSO provider", errUtils.ErrInvalidProviderConfig) + } @@ - if provider.Region == "" { - return fmt.Errorf("%w: region is required for AWS SSO provider", errUtils.ErrInvalidAuthConfig) - } + if provider.Region == "" { + return fmt.Errorf("%w: region is required for AWS SSO provider", errUtils.ErrInvalidProviderConfig) + }
175-187: Harden SAML URL validation and use provider-scoped sentinel.- if provider.URL == "" { - return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig) - } + if provider.URL == "" { + return fmt.Errorf("%w: url is required for AWS SAML provider", errUtils.ErrInvalidProviderConfig) + } @@ - if provider.Region == "" { - return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidAuthConfig) - } + if provider.Region == "" { + return fmt.Errorf("%w: region is required for AWS SAML provider", errUtils.ErrInvalidProviderConfig) + } @@ - // Validate URL format - if _, err := url.Parse(provider.URL); err != nil { - return fmt.Errorf("invalid URL format: %w", errors.Join(errUtils.ErrInvalidAuthConfig, err)) - } + // Validate URL format. + u, err := url.ParseRequestURI(provider.URL) + if err != nil || u.Scheme == "" || u.Host == "" { + return fmt.Errorf("%w: invalid URL format %q", errUtils.ErrInvalidProviderConfig, provider.URL) + }
94-95: Use %q for clarity in kind messages.- return fmt.Errorf("%w: unsupported provider kind: %s", errUtils.ErrInvalidProviderKind, provider.Kind) + return fmt.Errorf("%w: unsupported provider kind %q", errUtils.ErrInvalidProviderKind, provider.Kind)
136-142: Validate referenced identities exist before graphing edges.Dangling references should fail fast during validation.
for name, identity := range identities { if identity.Via != nil { if identity.Via.Identity != "" { - graph[name] = append(graph[name], identity.Via.Identity) + ref := identity.Via.Identity + if _, ok := identities[ref]; !ok { + return fmt.Errorf("%w: referenced identity %q does not exist", errUtils.ErrInvalidAuthConfig, ref) + } + graph[name] = append(graph[name], ref) } } }
🧹 Nitpick comments (9)
internal/auth/types/aws_credentials.go (3)
17-17: Use Go initialism style for MFA/ARN.Rename field to follow Go conventions.
- MfaArn string `json:"mfa_arn,omitempty"` + MFAArn string `json:"mfa_arn,omitempty"`Note: ripple rename across references.
20-21: Add a compile-time interface assertion.Makes the contract explicit and catches regressions at build time.
var _ ICredentials = (*AWSCredentials)(nil)
45-51: Silent parse failures in whoami builder.You’re intentionally ignoring the error; with the parsing hardening above this is fine. If parse still fails, consider emitting a trace/metric upstream (not UI).
internal/auth/validation/validator.go (6)
33-53: Precompute pointer maps once; reuse in loops and chain validation.Avoid repeated convertProviders/convertIdentities calls and tighten comments.
@@ - // Validate providers. + // Precompute pointer maps. + providersPtr := convertProviders(config.Providers) + identitiesPtr := convertIdentities(config.Identities) + + // Validate providers. @@ - for name, identity := range config.Identities { - if err := v.ValidateIdentity(name, &identity, convertProviders(config.Providers)); err != nil { + for name, identity := range config.Identities { + if err := v.ValidateIdentity(name, &identity, providersPtr); err != nil { return fmt.Errorf("identity %q validation failed: %w", name, errors.Join(errUtils.ErrInvalidAuthConfig, err)) } } @@ - // Validate chains - if err := v.ValidateChains(convertIdentities(config.Identities), convertProviders(config.Providers)); err != nil { + // Validate chains. + if err := v.ValidateChains(identitiesPtr, providersPtr); err != nil { return fmt.Errorf("identity chain validation failed: %w", errors.Join(errUtils.ErrInvalidAuthConfig, err)) }
127-127: Use identity-scoped sentinel for unsupported kinds.- return fmt.Errorf("%w: unsupported identity kind: %s", errUtils.ErrInvalidAuthConfig, identity.Kind) + return fmt.Errorf("%w: unsupported identity kind %q", errUtils.ErrInvalidIdentityConfig, identity.Kind)
193-196: Use provider-scoped sentinel for GitHub OIDC validation.- if provider.Region == "" { - return fmt.Errorf("%w: region is required for GitHub OIDC provider", errUtils.ErrInvalidAuthConfig) - } + if provider.Region == "" { + return fmt.Errorf("%w: region is required for GitHub OIDC provider", errUtils.ErrInvalidProviderConfig) + }
3-12: Accept all AWS partitions for assume-role ARN; add regexp import.Current check rejects gov/cn partitions and some valid ARNs.
@@ import ( "errors" "fmt" "net/url" + "regexp" "strings" @@ - if !strings.HasPrefix(roleArn, "arn:aws:iam::") { - return fmt.Errorf("%w: invalid role ARN format", errUtils.ErrInvalidAuthConfig) - } + // Accept aws, aws-us-gov, aws-cn, etc. + re := regexp.MustCompile(`^arn:[^:]+:iam::\d{12}:role\/.+$`) + if !re.MatchString(roleArn) { + return fmt.Errorf("%w: invalid role ARN format %q", errUtils.ErrInvalidIdentityConfig, roleArn) + }Also applies to: 237-239
61-61: Finish comment sentences with periods (godot).Keeps linters happy and docs tidy.
@@ - // Default to Info if not specified + // Default to Info if not specified. @@ - // Validate based on provider kind + // Validate based on provider kind. @@ - // Validate via configuration - AWS User identities don't require via provider + // Validate via configuration - AWS User identities don't require via provider. @@ - // TODO replace with Identity Interface Validate() - // Validate based on identity kind + // TODO replace with Identity Interface Validate(). + // Validate based on identity kind. @@ - // Build dependency graph + // Build dependency graph. @@ - // Check for cycles using DFS + // Check for cycles using DFS. @@ -// (aws/assume-role provider removed) +// (aws/assume-role provider removed). @@ - // Validate URL format + // Validate URL format. @@ - // User identities typically don't require additional validation + // User identities typically don't require additional validation.Also applies to: 85-85, 108-108, 117-118, 134-134, 144-144, 172-172, 184-184, 246-246
64-69: Normalize log level case (accept Debug/debug/etc.).Reduces user friction while preserving canonical values.
- validLevels := []string{"Debug", "Info", "Warn", "Error"} - for _, validLevel := range validLevels { - if logs.Level == validLevel { - return nil - } - } + validLevels := []string{"Debug", "Info", "Warn", "Error"} + for _, validLevel := range validLevels { + if strings.EqualFold(logs.Level, validLevel) { + // normalize to canonical casing. + logs.Level = validLevel + return nil + } + }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
internal/auth/identities/aws/permission_set.go(1 hunks)internal/auth/types/aws_credentials.go(1 hunks)internal/auth/validation/validator.go(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/auth/identities/aws/permission_set.go
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go
📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)
**/*.go: Use Viper for managing configuration, environment variables, and flags
Use interfaces for external dependencies to facilitate mocking
All code must pass golangci-lint checks
Follow Go's error handling idioms
Use meaningful error messages
Wrap errors with context using fmt.Errorf("context: %w", err)
Consider using a custom error type for domain-specific errors
Follow standard Go coding style
Use gofmt and goimports to format code
Prefer short, descriptive variable names
Use snake_case for environment variables
Document all exported functions, types, and methods
Document complex logic with inline comments
Follow Go's documentation conventions
Use Viper for configuration management
Support configuration via files, environment variables, and flags
Follow the precedence order: flags > environment variables > config file > defaults
**/*.go: All comments in Go code must end with periods (enforced by golangci-lint godot).
Wrap all returned errors using static errors from the errors package; never return dynamic errors directly.
Always bind environment variables with viper.BindEnv() and provide ATMOS_ alternatives for each external var.
Separate structured logging from UI output: use stderr for prompts/errors to user; stdout only for data; never use logging for UI.
Most text UI must go to stderr; only data/results to stdout. Prefer utils.PrintfMessageToTUI for UI messages.
For non-standard execution paths, capture telemetry with telemetry.CaptureCmd or telemetry.CaptureCmdString and never capture user data.
Ensure cross-platform compatibility: prefer SDKs to external binaries, use filepath/os/runtime for portability, and add build constraints when needed.
Files:
internal/auth/validation/validator.gointernal/auth/types/aws_credentials.go
🧠 Learnings (1)
📚 Learning: 2025-09-09T02:14:36.682Z
Learnt from: Benbentwo
PR: cloudposse/atmos#1452
File: internal/auth/types/whoami.go:14-15
Timestamp: 2025-09-09T02:14:36.682Z
Learning: The WhoamiInfo struct in internal/auth/types/whoami.go requires the Credentials field to be JSON-serializable for keystore unmarshaling operations, despite security concerns about credential exposure.
Applied to files:
internal/auth/types/aws_credentials.go
🧬 Code graph analysis (2)
internal/auth/validation/validator.go (3)
internal/auth/types/interfaces.go (3)
Validator(157-169)Provider(53-72)Identity(75-95)pkg/schema/schema.go (4)
AuthConfig(975-979)Logs(366-369)Provider(982-994)Identity(1002-1010)errors/errors.go (4)
ErrInvalidAuthConfig(83-83)ErrInvalidProviderConfig(87-87)ErrInvalidProviderKind(86-86)ErrInvalidIdentityConfig(85-85)
internal/auth/types/aws_credentials.go (2)
errors/errors.go (1)
ErrInvalidAuthConfig(83-83)internal/auth/types/whoami.go (1)
WhoamiInfo(6-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Build (ubuntu-latest, linux)
- GitHub Check: Build (windows-latest, windows)
- GitHub Check: Lint (golangci)
- GitHub Check: Analyze (go)
- GitHub Check: Summary
| AccessKeyID string `json:"access_key_id,omitempty"` | ||
| SecretAccessKey string `json:"secret_access_key,omitempty"` | ||
| SessionToken string `json:"session_token,omitempty"` | ||
| Region string `json:"region,omitempty"` | ||
| Expiration string `json:"expiration,omitempty"` | ||
| MfaArn string `json:"mfa_arn,omitempty"` | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Avoid leaking secrets in JSON outputs; provide a redacted view.
Keep this struct fully serializable for keystore needs, but expose a redacted copy for human-facing outputs (e.g., whoami) to prevent accidental disclosure.
Proposed helper (use in CLI/UI paths only):
// Redacted returns a copy with sensitive fields masked for display.
func (c AWSCredentials) Redacted() AWSCredentials {
rc := c
if len(rc.AccessKeyID) > 4 {
rc.AccessKeyID = rc.AccessKeyID[:4] + "…" + rc.AccessKeyID[len(rc.AccessKeyID)-4:]
}
if rc.SecretAccessKey != "" {
rc.SecretAccessKey = "REDACTED"
}
if rc.SessionToken != "" {
rc.SessionToken = "REDACTED"
}
return rc
}Validate that auth whoami doesn’t print secrets by default:
I can add unit tests covering redaction and ensure the default whoami JSON omits/masks secrets.
🏁 Script executed:
#!/bin/bash
# grep likely whoami paths and any inclusion of "credentials" in output builders
rg -nP -C2 '(whoami|Whoami)' --type go
rg -nP -C2 '"credentials"\s*:' --type goLength of output: 16608
🏁 Script executed:
#!/bin/bash
sed -n '1,200p' internal/auth/types/whoami.goLength of output: 630
🏁 Script executed:
#!/bin/bash
sed -n '580,650p' internal/auth/manager.goLength of output: 1498
Redact AWS credentials in JSON whoami output
In cmd/auth_whoami.go’s JSON branch (around lines 61–72), the WhoamiInfo.Credentials interface still serializes the full AWSCredentials (including access_key_id, secret_access_key and session_token). Apply a Redacted() helper (or custom JSON marshaller) on the concrete AWSCredentials to mask those fields before json.MarshalIndent. Add unit tests to assert no cleartext secrets appear in the JSON.
🤖 Prompt for AI Agents
internal/auth/types/aws_credentials.go around lines 12-18: the AWSCredentials
struct currently exposes sensitive fields (access_key_id, secret_access_key,
session_token); implement a Redacted() method on AWSCredentials that returns a
copy with those fields replaced by masked values (e.g. fixed "<redacted>" or
partial-show mask) or add a custom JSON marshaller that emits masked values for
those fields; then update cmd/auth_whoami.go where WhoamiInfo.Credentials is
serialized in the JSON branch to call the Redacted() helper (or rely on the new
marshaller) before json.MarshalIndent so secrets are not printed; add unit tests
that construct AWSCredentials, serialize the whoami JSON, and assert that the
output contains no full cleartext access_key_id/secret_access_key/session_token
and that masked placeholders are present.
| expTime, err := time.Parse(time.RFC3339, c.Expiration) | ||
| if err != nil { | ||
| return true | ||
| } | ||
| return time.Now().After(expTime) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Harden RFC3339 parsing to avoid false expirations.
AWS often returns fractional seconds; RFC3339-only parsing will fail and mark creds expired. Parse with RFC3339, then fallback to RFC3339Nano, and compare in UTC for safety.
func (c *AWSCredentials) IsExpired() bool {
if c.Expiration == "" {
return false
}
- expTime, err := time.Parse(time.RFC3339, c.Expiration)
- if err != nil {
- return true
- }
- return time.Now().After(expTime)
+ expTime, err := time.Parse(time.RFC3339, c.Expiration)
+ if err != nil {
+ if t2, err2 := time.Parse(time.RFC3339Nano, c.Expiration); err2 == nil {
+ expTime = t2
+ } else {
+ return true
+ }
+ }
+ return time.Now().UTC().After(expTime.UTC())
}
// GetExpiration implements ICredentials for AWSCredentials.
func (c *AWSCredentials) GetExpiration() (*time.Time, error) {
if c.Expiration == "" {
return nil, nil
}
- expTime, err := time.Parse(time.RFC3339, c.Expiration)
- if err != nil {
- return nil, fmt.Errorf("%w: failed parsing AWS credential expiration: %v", errUtils.ErrInvalidAuthConfig, err)
- }
- return &expTime, nil
+ expTime, err := time.Parse(time.RFC3339, c.Expiration)
+ if err != nil {
+ if t2, err2 := time.Parse(time.RFC3339Nano, c.Expiration); err2 == nil {
+ return &t2, nil
+ }
+ return nil, fmt.Errorf("%w: failed parsing AWS credential expiration value %q", errUtils.ErrInvalidAuthConfig, c.Expiration)
+ }
+ return &expTime, nil
}Also applies to: 38-42
| // Validate via configuration - AWS User identities don't require via provider | ||
| if identity.Kind != "aws/user" && identity.Via != nil { | ||
| if identity.Via.Provider != "" { | ||
| if _, exists := providers[identity.Via.Provider]; !exists { | ||
| return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Require via for non-user identities; enforce exactly one of provider or identity.
Prevents invalid chains and late failures.
- // Validate via configuration - AWS User identities don't require via provider
- if identity.Kind != "aws/user" && identity.Via != nil {
- if identity.Via.Provider != "" {
- if _, exists := providers[identity.Via.Provider]; !exists {
- return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider)
- }
- }
- }
+ // Validate via configuration - AWS User identities don't require via provider.
+ if identity.Kind != "aws/user" {
+ if identity.Via == nil {
+ return fmt.Errorf("%w: identity %q requires a via configuration", errUtils.ErrInvalidIdentityConfig, name)
+ }
+ // Exactly one of via.provider or via.identity must be set.
+ if (identity.Via.Provider == "" && identity.Via.Identity == "") || (identity.Via.Provider != "" && identity.Via.Identity != "") {
+ return fmt.Errorf("%w: identity %q must specify either via.provider or via.identity", errUtils.ErrInvalidIdentityConfig, name)
+ }
+ if identity.Via.Provider != "" {
+ if _, exists := providers[identity.Via.Provider]; !exists {
+ return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidIdentityConfig, identity.Via.Provider)
+ }
+ }
+ }If top-level non-user identities without via are intentionally supported, ignore this and let me know. I can tailor the rules accordingly.
🏁 Script executed:
#!/bin/bash
set -e
# Locate validator.go
file=$(fd --full-path validator.go | head -n1 || echo "")
if [[ -z "$file" ]]; then
echo "validator.go not found"
exit 1
fi
echo "Validator file path: $file"
# Print first 200 lines for context
echo "==== validator.go (first 200 lines) ===="
sed -n '1,200p' "$file"
# Find Identity struct definitions
echo "==== Identity struct definition(s) ===="
rg -nP 'type\s+\w*Identity\s+struct' -C3
# Find Via struct definitions
echo "==== Via struct definition(s) ===="
rg -nP 'type\s+\w*Via\s+struct' -C3
# Check for error constants
echo "==== ErrInvalidIdentityConfig occurrences ===="
rg -n "ErrInvalidIdentityConfig" || echo "None"
echo "==== ErrInvalidAuthConfig occurrences ===="
rg -n "ErrInvalidAuthConfig" || echo "None"Length of output: 19965
🏁 Script executed:
#!/bin/bash
set -e
# Show how Via is used in permission_set identity
rg -nP 'Via' -C3 internal/auth/identities/aws/permission_set.goLength of output: 417
🏁 Script executed:
#!/bin/bash
set -e
# Locate validator.go
file=$(fd --full-path validator.go | head -n1 || echo "")
if [[ -z "$file" ]]; then
echo "validator.go not found"
exit 1
fi
echo "validator.go: $file"
# Print validatePermissionSetIdentity signature and surrounding context
rg -n -C5 'func \(v \*validator\) validatePermissionSetIdentity' "$file" || echo "Function not found"
# Extract the implementation (100 lines from function start)
start_line=$(rg -n 'func \(v \*validator\) validatePermissionSetIdentity' --no-filename "$file" | cut -d: -f1)
if [[ -n "$start_line" ]]; then
sed -n "${start_line},$((start_line+100))p" "$file"
fiLength of output: 3296
🏁 Script executed:
#!/bin/bash
set -e
# Show the beginning of factory.go to identify identity instantiation logic
sed -n '1,200p' internal/auth/factory.go
# Find where permissionSetIdentity is instantiated or referenced in factory.go
rg -nP 'permissionSetIdentity' -C3 internal/auth/factory.goLength of output: 1667
🏁 Script executed:
#!/bin/bash
set -e
# Locate manager.go in internal/auth
file=$(fd --full-path manager.go | grep "internal/auth/manager.go" | head -n1)
echo "manager.go: $file"
# Print lines around where via is enforced in manager
sed -n '550,630p' "$file"Length of output: 2486
Enforce via presence and exclusive provider/identity in ValidateIdentity
Require non-aws/user identities to include a Via, and ensure exactly one of Via.Provider or Via.Identity is set to prevent invalid chains at runtime.
File: internal/auth/validation/validator.go
Replace lines 108–115 with:
- // Validate via configuration - AWS User identities don't require via provider
- if identity.Kind != "aws/user" && identity.Via != nil {
- if identity.Via.Provider != "" {
- if _, exists := providers[identity.Via.Provider]; !exists {
- return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider)
- }
- }
- }
+ // Validate via configuration - AWS User identities don't require via provider.
+ if identity.Kind != "aws/user" {
+ // Non-user identities must include a via configuration.
+ if identity.Via == nil {
+ return fmt.Errorf("%w: identity %q requires a via configuration", errUtils.ErrInvalidIdentityConfig, name)
+ }
+ // Exactly one of via.provider or via.identity must be set.
+ if (identity.Via.Provider == "" && identity.Via.Identity == "") || (identity.Via.Provider != "" && identity.Via.Identity != "") {
+ return fmt.Errorf("%w: identity %q must specify either via.provider or via.identity", errUtils.ErrInvalidIdentityConfig, name)
+ }
+ if identity.Via.Provider != "" {
+ if _, exists := providers[identity.Via.Provider]; !exists {
+ return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidIdentityConfig, identity.Via.Provider)
+ }
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Validate via configuration - AWS User identities don't require via provider | |
| if identity.Kind != "aws/user" && identity.Via != nil { | |
| if identity.Via.Provider != "" { | |
| if _, exists := providers[identity.Via.Provider]; !exists { | |
| return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider) | |
| } | |
| } | |
| } | |
| - // Validate via configuration - AWS User identities don't require via provider | |
| - if identity.Kind != "aws/user" && identity.Via != nil { | |
| - if identity.Via.Provider != "" { | |
| - if _, exists := providers[identity.Via.Provider]; !exists { | |
| - return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidAuthConfig, identity.Via.Provider) | |
| - } | |
| - } | |
| // Validate via configuration - AWS User identities don't require via provider. | |
| if identity.Kind != "aws/user" { | |
| // Non-user identities must include a via configuration. | |
| if identity.Via == nil { | |
| return fmt.Errorf("%w: identity %q requires a via configuration", errUtils.ErrInvalidIdentityConfig, name) | |
| } | |
| // Exactly one of via.provider or via.identity must be set. | |
| if (identity.Via.Provider == "" && identity.Via.Identity == "") || (identity.Via.Provider != "" && identity.Via.Identity != "") { | |
| return fmt.Errorf("%w: identity %q must specify either via.provider or via.identity", errUtils.ErrInvalidIdentityConfig, name) | |
| } | |
| if identity.Via.Provider != "" { | |
| if _, exists := providers[identity.Via.Provider]; !exists { | |
| return fmt.Errorf("%w: referenced provider %q does not exist", errUtils.ErrInvalidIdentityConfig, identity.Via.Provider) | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
internal/auth/validation/validator.go around lines 108-115: for non-`aws/user`
identities enforce that `Via` is present and that exactly one of `Via.Provider`
or `Via.Identity` is set; if `Via` is nil return fmt.Errorf("%w: non-aws/user
identity must include via", errUtils.ErrInvalidAuthConfig); compute count := 0;
increment for non-empty `Via.Provider` and non-empty `Via.Identity`; if count !=
1 return fmt.Errorf("%w: exactly one of via.provider or via.identity must be
set", errUtils.ErrInvalidAuthConfig); if `Via.Provider` is set verify the
provider exists in `providers` and return the existing provider-not-found error
if not; if `Via.Identity` is set verify the referenced identity exists in the
appropriate identity map/collection and return a clear invalid-config error if
it does not.
closes PR #1424
what
This PR introduces a complete authentication system for Atmos, enabling secure multi-cloud authentication with support for AWS IAM Identity Center (SSO), SAML providers, assume role chains, and AWS user credentials. The system provides seamless integration with Terraform workflows while maintaining isolation from users' existing AWS configurations.
Features
Authentication Providers
Identity Chaining
CLI Commands
atmos auth login- Interactive authentication with identity selectionatmos auth whoami- Display current authentication statusatmos auth env- Export environment variables (export, json, dotenv formats)atmos auth exec- Execute commands with authentication contextatmos auth validate- Comprehensive configuration validationatmos auth user configure- AWS user credential configurationAWS Integration
Architecture
Layout
Key Components
AuthManager: Central authentication orchestrator
Provider/Identity Pattern: Extensible authentication architecture
Credential Store: Encrypted credential caching with expiration
AWS File Manager: Isolated AWS credential/config file management
Configuration Merger: Component-level auth overrides
why
Atmos Auth allows us to simplify our reference architecture and helps assist with consolidating the toolset that the typical DevOps user needs to manage Terraform.
Summary by CodeRabbit
New Features
Improvements
Documentation
Tests
Chores