Skip to content
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
27 changes: 27 additions & 0 deletions internal/pkg/deploy/cloudformation/stack/env_integration_test.go
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
Loading