Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow assume role chaining with federated users (source identity and session tags) #5359

Open
wants to merge 9 commits into
base: mainline
Choose a base branch
from
34 changes: 22 additions & 12 deletions internal/pkg/cli/deploy/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,15 @@ type DeployEnvironmentInput struct {
Detach bool
}

// AdditionalAssumeRolePermissions helper method to export the additional assume role permissions of environment roles
// from the manifest.
func (dei DeployEnvironmentInput) AdditionalAssumeRolePermissions() []string {
if dei.Manifest == nil {
return []string{}
}
return dei.Manifest.AdditionalAssumeRolePermissions
}

// GenerateCloudFormationTemplate returns the environment stack's template and parameter configuration.
func (d *envDeployer) GenerateCloudFormationTemplate(in *DeployEnvironmentInput) (*GenerateCloudFormationTemplateOutput, error) {
stackInput, err := d.buildStackInput(in)
Expand Down Expand Up @@ -420,18 +429,19 @@ func (d *envDeployer) buildStackInput(in *DeployEnvironmentInput) (*cfnstack.Env
RootDomainHostedZoneId: d.app.DomainHostedZoneID,
AppDomainHostedZoneId: appHostedZoneID,
},
AdditionalTags: d.app.Tags,
Addons: addons,
CustomResourcesURLs: in.CustomResourcesURLs,
ArtifactBucketARN: awss3.FormatARN(partition.ID(), resources.S3Bucket),
ArtifactBucketKeyARN: resources.KMSKeyARN,
CIDRPrefixListIDs: cidrPrefixListIDs,
PublicALBSourceIPs: d.publicALBSourceIPs(in),
Mft: in.Manifest,
ForceUpdate: in.ForceNewUpdate,
RawMft: in.RawManifest,
PermissionsBoundary: in.PermissionsBoundary,
Version: in.Version,
AdditionalTags: d.app.Tags,
Addons: addons,
CustomResourcesURLs: in.CustomResourcesURLs,
ArtifactBucketARN: awss3.FormatARN(partition.ID(), resources.S3Bucket),
ArtifactBucketKeyARN: resources.KMSKeyARN,
CIDRPrefixListIDs: cidrPrefixListIDs,
PublicALBSourceIPs: d.publicALBSourceIPs(in),
Mft: in.Manifest,
AdditionalAssumeRolePermissions: in.AdditionalAssumeRolePermissions(),
ForceUpdate: in.ForceNewUpdate,
RawMft: in.RawManifest,
PermissionsBoundary: in.PermissionsBoundary,
Version: in.Version,
}, nil
}

Expand Down
30 changes: 21 additions & 9 deletions internal/pkg/cli/env_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ type initEnvVars struct {
importCerts []string // Additional existing ACM certificates to use.
internalALBSubnets []string // Subnets to be used for internal ALB placement.
allowVPCIngress bool // True means the env stack will create ingress to the internal ALB from ports 80/443.
federatedSession bool // True means, that the following additional permissions are added to the environment manager role trust policy: sts:SetSourceIdentity, sts:TagSession.

tempCreds tempCredsVars // Temporary credentials to initialize the environment. Mutually exclusive with the profile.
region string // The region to create the environment in.
Expand Down Expand Up @@ -747,10 +748,11 @@ func (o *initEnvOpts) deployEnv(app *config.Application) error {
Domain: app.Domain,
AccountPrincipalARN: caller.RootUserARN,
},
AdditionalTags: app.Tags,
ArtifactBucketARN: artifactBucketARN,
ArtifactBucketKeyARN: resources.KMSKeyARN,
PermissionsBoundary: app.PermissionsBoundary,
AdditionalTags: app.Tags,
ArtifactBucketARN: artifactBucketARN,
ArtifactBucketKeyARN: resources.KMSKeyARN,
PermissionsBoundary: app.PermissionsBoundary,
AdditionalAssumeRolePermissions: o.additionalAssumeRolePermissions(),
}

if err := o.cleanUpDanglingRoles(o.appName, o.name); err != nil {
Expand All @@ -770,6 +772,13 @@ func (o *initEnvOpts) deployEnv(app *config.Application) error {
return nil
}

func (o *initEnvOpts) additionalAssumeRolePermissions() (permissions []string) {
if o.federatedSession {
permissions = append(permissions, "sts:SetSourceIdentity", "sts:TagSession")
}
return
}

func (o *initEnvOpts) addToStackset(opts *deploycfn.AddEnvToAppOpts) error {
if err := o.appDeployer.AddEnvToApp(opts); err != nil {
return fmt.Errorf("add env %s to application %s: %w", opts.EnvName, opts.App.Name, err)
Expand Down Expand Up @@ -867,11 +876,12 @@ func (o *initEnvOpts) tryDeletingEnvRoles(app, env string) {

func (o *initEnvOpts) writeManifest() (string, error) {
customizedEnv := &config.CustomizeEnv{
ImportVPC: o.importVPCConfig(),
VPCConfig: o.adjustVPCConfig(),
ImportCertARNs: o.importCerts,
InternalALBSubnets: o.internalALBSubnets,
EnableInternalALBVPCIngress: o.allowVPCIngress,
ImportVPC: o.importVPCConfig(),
VPCConfig: o.adjustVPCConfig(),
ImportCertARNs: o.importCerts,
InternalALBSubnets: o.internalALBSubnets,
EnableInternalALBVPCIngress: o.allowVPCIngress,
AdditionalAssumeRolePermissions: o.additionalAssumeRolePermissions(),
}
if customizedEnv.IsEmpty() {
customizedEnv = nil
Expand Down Expand Up @@ -973,6 +983,7 @@ func buildEnvInitCmd() *cobra.Command {
cmd.Flags().StringSliceVar(&vars.internalALBSubnets, internalALBSubnetsFlag, nil, internalALBSubnetsFlagDescription)
cmd.Flags().BoolVar(&vars.allowVPCIngress, allowVPCIngressFlag, false, allowVPCIngressFlagDescription)
cmd.Flags().BoolVar(&vars.defaultConfig, defaultConfigFlag, false, defaultConfigFlagDescription)
cmd.Flags().BoolVar(&vars.federatedSession, allowFederatedSessionFlag, false, allowFederatedSessionFlagDescription)

flags := pflag.NewFlagSet("Common", pflag.ContinueOnError)
flags.AddFlag(cmd.Flags().Lookup(appFlag))
Expand All @@ -984,6 +995,7 @@ func buildEnvInitCmd() *cobra.Command {
flags.AddFlag(cmd.Flags().Lookup(regionFlag))
flags.AddFlag(cmd.Flags().Lookup(defaultConfigFlag))
flags.AddFlag(cmd.Flags().Lookup(allowDowngradeFlag))
flags.AddFlag(cmd.Flags().Lookup(allowFederatedSessionFlag))

resourcesImportFlags := pflag.NewFlagSet("Import Existing Resources", pflag.ContinueOnError)
resourcesImportFlags.AddFlag(cmd.Flags().Lookup(vpcIDFlag))
Expand Down
3 changes: 3 additions & 0 deletions internal/pkg/cli/env_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,7 @@ func TestInitEnvOpts_Execute(t *testing.T) {
testCases := map[string]struct {
enableContainerInsights bool
allowDowngrade bool
allowFederatedSession bool
setupMocks func(m *initEnvExecuteMocks)
wantedErrorS string
}{
Expand Down Expand Up @@ -1154,6 +1155,7 @@ func TestInitEnvOpts_Execute(t *testing.T) {
"success": {
enableContainerInsights: true,
allowDowngrade: true,
allowFederatedSession: true,
setupMocks: func(m *initEnvExecuteMocks) {
m.store.EXPECT().GetApplication("phonetool").Return(&config.Application{Name: "phonetool"}, nil)
m.store.EXPECT().CreateEnvironment(&config.Environment{
Expand Down Expand Up @@ -1338,6 +1340,7 @@ func TestInitEnvOpts_Execute(t *testing.T) {
EnableContainerInsights: tc.enableContainerInsights,
},
allowAppDowngrade: tc.allowDowngrade,
federatedSession: tc.allowFederatedSession,
},
store: m.store,
envDeployer: m.deployer,
Expand Down
2 changes: 2 additions & 0 deletions internal/pkg/cli/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const (

enableContainerInsightsFlag = "container-insights"
defaultConfigFlag = "default-config"
allowFederatedSessionFlag = "federated-session"

accessKeyIDFlag = "aws-access-key-id"
secretAccessKeyFlag = "aws-secret-access-key"
Expand Down Expand Up @@ -406,6 +407,7 @@ Cannot be specified with --default-config or any of the --override flags.`

enableContainerInsightsFlagDescription = "Optional. Enable CloudWatch Container Insights."
defaultConfigFlagDescription = "Optional. Skip prompting and use default environment configuration."
allowFederatedSessionFlagDescription = "Optional. Shorthand to add additional permissions to the Assume Role policy required for federated sessions with a source identity or transitive session tags."

profileFlagDescription = "Name of the profile for the environment account."
accessKeyIDFlagDescription = "Optional. An AWS access key for the environment account."
Expand Down
1 change: 1 addition & 0 deletions internal/pkg/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ func (o *initOpts) deployEnv() error {
// Set the application name from app init to the env init command, and check whether a flag has been passed for envName.
initEnvCmd.appName = *o.appName
initEnvCmd.name = o.initVars.envName
initEnvCmd.federatedSession = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should probably keep it consistent with the default value in env init which is false. I would assume relatively few users would need to set this and they could use env init instead of init.

}

if err := o.askEnvNameAndMaybeInit(); err != nil {
Expand Down
13 changes: 7 additions & 6 deletions internal/pkg/config/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,20 @@ type Environment struct {

// CustomizeEnv represents the custom environment config.
type CustomizeEnv struct {
ImportVPC *ImportVPC `json:"importVPC,omitempty"`
VPCConfig *AdjustVPC `json:"adjustVPC,omitempty"`
ImportCertARNs []string `json:"importCertARNs,omitempty"`
InternalALBSubnets []string `json:"internalALBSubnets,omitempty"`
EnableInternalALBVPCIngress bool `json:"enableInternalALBVPCIngress,omitempty"`
ImportVPC *ImportVPC `json:"importVPC,omitempty"`
VPCConfig *AdjustVPC `json:"adjustVPC,omitempty"`
ImportCertARNs []string `json:"importCertARNs,omitempty"`
InternalALBSubnets []string `json:"internalALBSubnets,omitempty"`
EnableInternalALBVPCIngress bool `json:"enableInternalALBVPCIngress,omitempty"`
AdditionalAssumeRolePermissions []string `json:"additionalAssumeRolePermissions,omitempty"`
}

// IsEmpty returns true if CustomizeEnv is an empty struct.
func (c *CustomizeEnv) IsEmpty() bool {
if c == nil {
return true
}
return c.ImportVPC == nil && c.VPCConfig == nil && len(c.ImportCertARNs) == 0 && len(c.InternalALBSubnets) == 0 && !c.EnableInternalALBVPCIngress
return c.ImportVPC == nil && c.VPCConfig == nil && len(c.ImportCertARNs) == 0 && len(c.InternalALBSubnets) == 0 && !c.EnableInternalALBVPCIngress && len(c.AdditionalAssumeRolePermissions) == 0
}

// ImportVPC holds the fields to import VPC resources.
Expand Down
57 changes: 30 additions & 27 deletions internal/pkg/deploy/cloudformation/stack/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,19 @@ type EnvConfig struct {
CustomResourcesURLs map[string]string // Mapping of Custom Resource Function Name to the S3 URL where the function zip file is stored.

// User inputs.
ImportVPCConfig *config.ImportVPC // Optional configuration if users have an existing VPC.
AdjustVPCConfig *config.AdjustVPC // Optional configuration if users want to override default VPC configuration.
ImportCertARNs []string // Optional configuration if users want to import certificates.
InternalALBSubnets []string // Optional configuration if users want to specify internal ALB placement.
AllowVPCIngress bool // Optional configuration to allow access to internal ALB from ports 80/443.
CIDRPrefixListIDs []string // Optional configuration to specify public security group ingress based on prefix lists.
PublicALBSourceIPs []string // Optional configuration to specify public security group ingress based on customer given source IPs.
InternalLBSourceIPs []string // Optional configuration to specify private security group ingress based on customer given source IPs.
Telemetry *config.Telemetry // Optional observability and monitoring configuration.
Mft *manifest.Environment // Unmarshaled and interpolated manifest object.
RawMft string // Content of the environment manifest with env var interpolation only.
ForceUpdate bool
ImportVPCConfig *config.ImportVPC // Optional configuration if users have an existing VPC.
AdjustVPCConfig *config.AdjustVPC // Optional configuration if users want to override default VPC configuration.
ImportCertARNs []string // Optional configuration if users want to import certificates.
InternalALBSubnets []string // Optional configuration if users want to specify internal ALB placement.
AllowVPCIngress bool // Optional configuration to allow access to internal ALB from ports 80/443.
CIDRPrefixListIDs []string // Optional configuration to specify public security group ingress based on prefix lists.
PublicALBSourceIPs []string // Optional configuration to specify public security group ingress based on customer given source IPs.
InternalLBSourceIPs []string // Optional configuration to specify private security group ingress based on customer given source IPs.
Telemetry *config.Telemetry // Optional observability and monitoring configuration.
AdditionalAssumeRolePermissions []string // Optional configuration to specify additional permissions to put into the Environment Manager Role for that environment.
Mft *manifest.Environment // Unmarshaled and interpolated manifest object.
RawMft string // Content of the environment manifest with env var interpolation only.
ForceUpdate bool
}

func (cfg *EnvConfig) loadCustomResourceURLs(crs []uploadable) error {
Expand Down Expand Up @@ -202,18 +203,19 @@ func (e *Env) Template() (string, error) {
forceUpdateID = id.String()
}
content, err := e.parser.ParseEnv(&template.EnvOpts{
AppName: e.in.App.Name,
EnvName: e.in.Name,
CustomResources: crs,
Addons: addons,
ArtifactBucketARN: e.in.ArtifactBucketARN,
ArtifactBucketKeyARN: e.in.ArtifactBucketKeyARN,
PermissionsBoundary: e.in.PermissionsBoundary,
PublicHTTPConfig: e.publicHTTPConfig(),
VPCConfig: vpcConfig,
PrivateHTTPConfig: e.privateHTTPConfig(),
Telemetry: e.telemetryConfig(),
CDNConfig: e.cdnConfig(),
AppName: e.in.App.Name,
EnvName: e.in.Name,
CustomResources: crs,
Addons: addons,
ArtifactBucketARN: e.in.ArtifactBucketARN,
ArtifactBucketKeyARN: e.in.ArtifactBucketKeyARN,
PermissionsBoundary: e.in.PermissionsBoundary,
PublicHTTPConfig: e.publicHTTPConfig(),
VPCConfig: vpcConfig,
PrivateHTTPConfig: e.privateHTTPConfig(),
Telemetry: e.telemetryConfig(),
CDNConfig: e.cdnConfig(),
AdditionalAssumeRolePermissions: e.in.AdditionalAssumeRolePermissions,

LatestVersion: e.in.Version,
SerializedManifest: string(e.in.RawMft),
Expand Down Expand Up @@ -414,9 +416,10 @@ type BootstrapEnv Env
// Template returns the CloudFormation template to bootstrap environment resources.
func (e *BootstrapEnv) Template() (string, error) {
content, err := e.parser.ParseEnvBootstrap(&template.EnvOpts{
ArtifactBucketARN: e.in.ArtifactBucketARN,
ArtifactBucketKeyARN: e.in.ArtifactBucketKeyARN,
PermissionsBoundary: e.in.PermissionsBoundary,
ArtifactBucketARN: e.in.ArtifactBucketARN,
ArtifactBucketKeyARN: e.in.ArtifactBucketKeyARN,
PermissionsBoundary: e.in.PermissionsBoundary,
AdditionalAssumeRolePermissions: e.in.AdditionalAssumeRolePermissions,
})
if err != nil {
return "", err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,33 @@ network:
}(),
wantedFileName: "template-with-importedvpc-flowlogs.yml",
},
"generate template with additional assume role permissions": {
input: func() *stack.EnvConfig {
rawMft := `name: test
type: Environment

additionalAssumeRolePermissions:
- sts:SetSourceIdentity
- sts:TagSession`
var mft manifest.Environment
err := yaml.Unmarshal([]byte(rawMft), &mft)
require.NoError(t, err)
return &stack.EnvConfig{
Version: "1.x",
App: deploy.AppInformation{
AccountPrincipalARN: "arn:aws:iam::000000000:root",
Name: "demo",
},
Name: "test",
ArtifactBucketARN: "arn:aws:s3:::mockbucket",
ArtifactBucketKeyARN: "arn:aws:kms:us-west-2:000000000:key/1234abcd-12ab-34cd-56ef-1234567890ab",
Mft: &mft,
AdditionalAssumeRolePermissions: mft.AdditionalAssumeRolePermissions,
RawMft: rawMft,
}
}(),
wantedFileName: "template-with-additional-assume-role-permissions.yml",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
Expand Down
26 changes: 21 additions & 5 deletions internal/pkg/deploy/cloudformation/stack/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,11 @@ func TestEnv_Template(t *testing.T) {
Telemetry: &template.Telemetry{
EnableContainerInsights: false,
},
ArtifactBucketARN: "arn:aws:s3:::mockbucket",
SerializedManifest: "name: env\ntype: Environment\n",
ForceUpdateID: "mockPreviousForceUpdateID",
DelegateDNS: true,
ArtifactBucketARN: "arn:aws:s3:::mockbucket",
SerializedManifest: "name: env\ntype: Environment\n",
ForceUpdateID: "mockPreviousForceUpdateID",
DelegateDNS: true,
AdditionalAssumeRolePermissions: []string{"sts:TagSession", "sts:SetSourceIdentity"},
HostedZones: &template.HostedZones{
RootDomainHostedZoneId: "Z00ABC",
AppDomainHostedZoneId: "Z00DEF",
Expand Down Expand Up @@ -1090,6 +1091,20 @@ func TestBootstrapEnv_Template(t *testing.T) {
},
expectedOutput: "mockTemplate",
},
"should contain additional permissions": {
in: &EnvConfig{
AdditionalAssumeRolePermissions: []string{"sts:TagSession", "sts:SetSourceIdentity"},
},
setupMock: func(m *mocks.MockenvReadParser) {
m.EXPECT().ParseEnvBootstrap(gomock.Any(), gomock.Any()).DoAndReturn(func(data *template.EnvOpts, options ...template.ParseOption) (*template.Content, error) {
require.Equal(t, &template.EnvOpts{
AdditionalAssumeRolePermissions: []string{"sts:TagSession", "sts:SetSourceIdentity"},
}, data)
return &template.Content{Buffer: bytes.NewBufferString("mockTemplate")}, nil
})
},
expectedOutput: "mockTemplate",
},
}

for name, tc := range testCases {
Expand Down Expand Up @@ -1275,7 +1290,8 @@ func mockDeployEnvironmentInput() *EnvConfig {
"DNSDelegationFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/mockkey2",
"CustomDomainFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/mockkey4",
},
ArtifactBucketARN: "arn:aws:s3:::mockbucket",
ArtifactBucketARN: "arn:aws:s3:::mockbucket",
AdditionalAssumeRolePermissions: []string{"sts:TagSession", "sts:SetSourceIdentity"},
Mft: &manifest.Environment{
Workload: manifest.Workload{
Name: aws.String("env"),
Expand Down