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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions cmd/auth_console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,14 @@ func (m *mockAuthManagerForProvider) GetIntegration(integrationName string) (*sc
return nil, errUtils.ErrNotImplemented
}

func (m *mockAuthManagerForProvider) ResolvePrincipalSetting(identityName, key string) (interface{}, bool) {
return nil, false
}

func (m *mockAuthManagerForProvider) ResolveProviderConfig(identityName string) (*schema.Provider, bool) {
return nil, false
}

// mockAuthManagerForIdentity implements minimal AuthManager for testing resolveIdentityName.
// Only GetDefaultIdentity is implemented - other methods return ErrNotImplemented
// because they are not needed by TestResolveIdentityName.
Expand Down Expand Up @@ -911,6 +919,14 @@ func (m *mockAuthManagerForIdentity) GetIntegration(integrationName string) (*sc
return nil, errUtils.ErrNotImplemented
}

func (m *mockAuthManagerForIdentity) ResolvePrincipalSetting(identityName, key string) (interface{}, bool) {
return nil, false
}

func (m *mockAuthManagerForIdentity) ResolveProviderConfig(identityName string) (*schema.Provider, bool) {
return nil, false
}

func TestResolveConsoleDuration(t *testing.T) {
_ = NewTestKit(t)

Expand Down
7 changes: 2 additions & 5 deletions cmd/auth_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,8 @@ func TestAuthCommandCompletion(t *testing.T) {
// Call the completion function.
completions, directive := completionFunc(authEnvCmd, []string{}, "")

// Verify we get the expected formats.
assert.Equal(t, 3, len(completions))
assert.Contains(t, completions, "json")
assert.Contains(t, completions, "bash")
assert.Contains(t, completions, "dotenv")
// Verify we get the expected formats (must match SupportedFormats in auth_env.go).
assert.ElementsMatch(t, SupportedFormats, completions)
assert.Equal(t, 4, int(directive)) // ShellCompDirectiveNoFileComp
})

Expand Down
26 changes: 22 additions & 4 deletions docs/prd/aws-auth-file-isolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,18 @@ output = json
- Purpose: Selects which profile section to use within the credentials/config files
- Note: Profile name matches identity name for consistency

**`AWS_REGION`** - Region override (optional)
**`AWS_REGION`** - Region from identity/provider configuration
- Example: `us-east-1`
- Purpose: Overrides region from config file
- Supports component-level overrides via stack inheritance
- Purpose: Sets region for AWS SDK operations
- Exported by: `atmos auth env` (when region is explicitly configured)
- Also available via: `!env AWS_REGION` in stack configurations
- Note: Only exported when region is explicitly configured in identity or provider; no default fallback

**`AWS_DEFAULT_REGION`** - Same as AWS_REGION (for SDK compatibility)
- Example: `us-east-1`
- Purpose: Fallback region for older AWS SDKs/tools
- Exported by: `atmos auth env` (when region is explicitly configured)
- Note: Set to same value as AWS_REGION for compatibility with legacy tools

### Conflicting Variables Cleared

Expand Down Expand Up @@ -229,12 +237,21 @@ Logic flow:
1. Create copy of input environment (doesn't mutate input)
2. Clear conflicting AWS credential env vars
3. Set `AWS_SHARED_CREDENTIALS_FILE`, `AWS_CONFIG_FILE`, `AWS_PROFILE`
4. Set `AWS_REGION` if provided
4. Set `AWS_REGION` and `AWS_DEFAULT_REGION` if region is explicitly configured
5. Set `AWS_EC2_METADATA_DISABLED=true`
6. Return new map

**Key Feature:** Returns NEW map instead of mutating input for safety and testability.

**Identity `Environment()` Method:**

The `Environment()` method on AWS identities returns environment variables for `atmos auth env`:
- Always returns: `AWS_SHARED_CREDENTIALS_FILE`, `AWS_CONFIG_FILE`, `AWS_PROFILE`
- Conditionally returns: `AWS_REGION`, `AWS_DEFAULT_REGION` (only when region is explicitly configured)
- Does NOT use default fallback region - exports only explicitly configured values

This enables users to reference `!env AWS_REGION` in stack configurations after sourcing auth environment variables.

### Auth Context Schema (`pkg/schema/schema.go`)

```go
Expand Down Expand Up @@ -539,3 +556,4 @@ AWS authentication successfully implements the universal pattern:
| Date | Version | Changes |
|------|---------|---------|
| 2025-01-XX | 1.0 | Initial AWS implementation PRD created to document existing implementation |
| 2025-01-12 | 1.1 | Added AWS_REGION and AWS_DEFAULT_REGION export via `atmos auth env` when region is explicitly configured |
1 change: 1 addition & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ var (
ErrCopyFile = errors.New("failed to copy file")
ErrCreateDirectory = errors.New("failed to create directory")
ErrOpenFile = errors.New("failed to open file")
ErrWriteFile = errors.New("failed to write to file")
ErrStatFile = errors.New("failed to stat file")
ErrRemoveDirectory = errors.New("failed to remove directory")
ErrSetPermissions = errors.New("failed to set permissions")
Expand Down
1 change: 0 additions & 1 deletion internal/exec/packer.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ func ExecutePacker(
}

// Check if the component is locked (`metadata.locked` is set to true).
// For Packer, only `build` modifies external resources.
if info.ComponentIsLocked && info.SubCommand == "build" {
return fmt.Errorf("%w: component '%s' cannot be modified (metadata.locked: true)",
errUtils.ErrLockedComponentCantBeProvisioned,
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/packer_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func ExecutePackerOutput(

manifestPath := filepath.Join(componentPath, manifestFilename)
if !u.FileExists(manifestPath) {
return nil, fmt.Errorf("%w: '%s' does not exist - it is generated by Packer when executing 'atmos packer build' and the manifest filename is specified in the 'manifest_file_name' variable of the Atmos component",
return nil, fmt.Errorf("%w: '%s' (generated by 'atmos packer build', filename configured via 'manifest_file_name' variable)",
errUtils.ErrMissingPackerManifest,
filepath.Join(atmosConfig.Components.Packer.BasePath, info.ComponentFolderPrefix, info.FinalComponent, manifestFilename),
)
Expand Down
46 changes: 46 additions & 0 deletions internal/exec/terraform_output_authcontext_wrapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,49 @@ func TestAuthContextWrapperGetStackInfo(t *testing.T) {
assert.Equal(t, "test-identity", stackInfo.AuthContext.AWS.Profile)
assert.Equal(t, "us-west-2", stackInfo.AuthContext.AWS.Region)
}

// TestAuthContextWrapperResolvePrincipalSetting verifies ResolvePrincipalSetting returns nil, false.
// The wrapper doesn't have access to identity/provider configuration, only auth context.
func TestAuthContextWrapperResolvePrincipalSetting(t *testing.T) {
authContext := &schema.AuthContext{
AWS: &schema.AWSAuthContext{
Profile: "test-identity",
Region: "us-west-2",
},
}

wrapper := newAuthContextWrapper(authContext)

// ResolvePrincipalSetting should always return nil, false for the wrapper.
// It only propagates existing auth context, not full identity configuration.
val, found := wrapper.ResolvePrincipalSetting("any-identity", "region")
assert.Nil(t, val, "ResolvePrincipalSetting should return nil")
assert.False(t, found, "ResolvePrincipalSetting should return false")

val, found = wrapper.ResolvePrincipalSetting("test-identity", "any-key")
assert.Nil(t, val, "ResolvePrincipalSetting should return nil for any key")
assert.False(t, found, "ResolvePrincipalSetting should return false for any key")
}

// TestAuthContextWrapperResolveProviderConfig verifies ResolveProviderConfig returns nil, false.
// The wrapper doesn't have access to provider configuration.
func TestAuthContextWrapperResolveProviderConfig(t *testing.T) {
authContext := &schema.AuthContext{
AWS: &schema.AWSAuthContext{
Profile: "test-identity",
Region: "us-west-2",
},
}

wrapper := newAuthContextWrapper(authContext)

// ResolveProviderConfig should always return nil, false for the wrapper.
// It only propagates existing auth context, not provider configuration.
provider, found := wrapper.ResolveProviderConfig("any-identity")
assert.Nil(t, provider, "ResolveProviderConfig should return nil")
assert.False(t, found, "ResolveProviderConfig should return false")

provider, found = wrapper.ResolveProviderConfig("test-identity")
assert.Nil(t, provider, "ResolveProviderConfig should return nil for any identity")
assert.False(t, found, "ResolveProviderConfig should return false for any identity")
}
16 changes: 16 additions & 0 deletions internal/exec/terraform_output_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,22 @@ func (a *authContextWrapper) GetIntegration(integrationName string) (*schema.Int
panic("authContextWrapper.GetIntegration should not be called")
}

func (a *authContextWrapper) ResolvePrincipalSetting(identityName, key string) (interface{}, bool) {
defer perf.Track(nil, "exec.authContextWrapper.ResolvePrincipalSetting")()

// Return false - this wrapper doesn't have access to identity/provider configuration.
// It only propagates existing auth context for nested component resolution.
return nil, false
}

func (a *authContextWrapper) ResolveProviderConfig(identityName string) (*schema.Provider, bool) {
defer perf.Track(nil, "exec.authContextWrapper.ResolveProviderConfig")()

// Return false - this wrapper doesn't have access to provider configuration.
// It only propagates existing auth context for nested component resolution.
return nil, false
}

// newAuthContextWrapper creates an AuthManager wrapper that returns the given AuthContext.
func newAuthContextWrapper(authContext *schema.AuthContext) *authContextWrapper {
if authContext == nil {
Expand Down
8 changes: 8 additions & 0 deletions pkg/auth/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ func (s *stubAuthManager) GetIntegration(integrationName string) (*schema.Integr
return nil, nil
}

func (s *stubAuthManager) ResolvePrincipalSetting(identityName, key string) (interface{}, bool) {
return nil, false
}

func (s *stubAuthManager) ResolveProviderConfig(identityName string) (*schema.Provider, bool) {
return nil, false
}

func TestGetConfigLogLevels(t *testing.T) {
tests := []struct {
name string
Expand Down
41 changes: 39 additions & 2 deletions pkg/auth/identities/aws/assume_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,13 @@ func (i *assumeRoleIdentity) Environment() (map[string]string, error) {
env[envVar.Key] = envVar.Value
}

// Resolve region through identity chain inheritance.
// First checks identity principal, then parent identities, then provider.
if region := i.resolveRegion(); region != "" {
env["AWS_REGION"] = region
env["AWS_DEFAULT_REGION"] = region
}

// Add environment variables from identity config.
for _, envVar := range i.config.Env {
env[envVar.Key] = envVar.Value
Expand Down Expand Up @@ -413,8 +420,8 @@ func (i *assumeRoleIdentity) PrepareEnvironment(ctx context.Context, environ map
credentialsFile := awsFileManager.GetCredentialsPath(providerName)
configFile := awsFileManager.GetConfigPath(providerName)

// Get region from identity if available.
region := i.region
// Resolve region through identity chain inheritance.
region := i.resolveRegion()

// Use shared AWS environment preparation helper.
return awsCloud.PrepareEnvironment(environ, i.name, credentialsFile, configFile, region), nil
Expand Down Expand Up @@ -467,6 +474,36 @@ func (i *assumeRoleIdentity) getRootProviderFromVia() (string, error) {
return "", fmt.Errorf("%w: cannot determine root provider for identity %q before authentication", errUtils.ErrInvalidAuthConfig, i.name)
}

// resolveRegion resolves the AWS region by traversing the identity chain.
// First checks identity chain for region setting, then falls back to provider's region.
// This uses the manager's generic chain resolution methods to support inheritance.
func (i *assumeRoleIdentity) resolveRegion() string {
// If manager is not available, fall back to direct config check or cached region.
if i.manager == nil {
if i.region != "" {
return i.region
}
if region, ok := i.config.Principal["region"].(string); ok && region != "" {
return region
}
return ""
}

// First check identity chain for region setting.
if val, ok := i.manager.ResolvePrincipalSetting(i.name, "region"); ok {
if region, ok := val.(string); ok && region != "" {
return region
}
}

// Fall back to provider's region.
if provider, ok := i.manager.ResolveProviderConfig(i.name); ok {
return provider.Region
}

return ""
}

// SetManagerAndProvider sets the manager and root provider name on the identity.
// This is used when loading cached credentials to allow the identity to resolve provider information.
func (i *assumeRoleIdentity) SetManagerAndProvider(manager types.AuthManager, rootProviderName string) {
Expand Down
Loading
Loading