diff --git a/Taskfile.yml b/Taskfile.yml index 3e4a97a..84b4e9c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -233,7 +233,7 @@ tasks: cmds: # yamllint disable-line rule:quoted-strings - 'gotestsum --format-hivis --format {{.FORMAT}} --jsonfile "testresults.json" ./pkg/domain ./pkg/infrastructure/ARMTemplateShared ./pkg/infrastructure/mpfSharedUtils ./pkg/infrastructure/authorizationCheckers/terraform -p {{numCPU}} -timeout 5m -ldflags="{{.LDFLAGS}}" -coverprofile="coverage.out" -covermode atomic' - - go test ./e2eTests -run TestE2EIdentitySelector -count=1 + - go test ./e2eTests -run 'TestE2E(IdentitySelector|TerraformBootstrapPermissions)' -count=1 - task: _test:getcover vars: TEST_NAME: "{{if gt (len (splitArgs .CLI_ARGS)) 0}}{{index (splitArgs .CLI_ARGS) 0}}{{end}}" diff --git a/e2eTests/e2eTerraformAuthPermissionMismatch_test.go b/e2eTests/e2eTerraformAuthPermissionMismatch_test.go index 58a7c9e..b8d1ee0 100644 --- a/e2eTests/e2eTerraformAuthPermissionMismatch_test.go +++ b/e2eTests/e2eTerraformAuthPermissionMismatch_test.go @@ -75,8 +75,7 @@ func TestTerraformAuthorizationPermissionMismatch(t *testing.T) { var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner var mpfService *usecase.MPFService - initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} - permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} + initialPermissionsToAdd, permissionsToAddToResult := getTerraformE2EBootstrapPermissions() deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "") mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) @@ -86,5 +85,5 @@ func TestTerraformAuthorizationPermissionMismatch(t *testing.T) { } assert.NotEmpty(t, mpfResult.RequiredPermissions) - assert.Equal(t, 11, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) + assert.Equal(t, 12, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) } diff --git a/e2eTests/e2eTerraformAuthorizationRequestDenied_test.go b/e2eTests/e2eTerraformAuthorizationRequestDenied_test.go index fc3f324..074f577 100644 --- a/e2eTests/e2eTerraformAuthorizationRequestDenied_test.go +++ b/e2eTests/e2eTerraformAuthorizationRequestDenied_test.go @@ -71,8 +71,7 @@ func TestTerraformAuthorizationRequestDenied(t *testing.T) { var rgManager usecase.ResourceGroupManager = rgm.NewResourceGroupManager(mpfArgs.SubscriptionID) var spRoleAssignmentManager usecase.ServicePrincipalRolemAssignmentManager = spram.NewSPRoleAssignmentManager(mpfArgs.SubscriptionID) - initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} - permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} + initialPermissionsToAdd, permissionsToAddToResult := getTerraformE2EBootstrapPermissions() deploymentAuthorizationCheckerCleaner := terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "") mpfService := usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) diff --git a/e2eTests/e2eTerraformInvalid_test.go b/e2eTests/e2eTerraformInvalid_test.go index 4c2abfd..a1b4e03 100644 --- a/e2eTests/e2eTerraformInvalid_test.go +++ b/e2eTests/e2eTerraformInvalid_test.go @@ -75,8 +75,7 @@ func TestTerraformACIInvalidVarFile(t *testing.T) { var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner var mpfService *usecase.MPFService - initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} - permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} + initialPermissionsToAdd, permissionsToAddToResult := getTerraformE2EBootstrapPermissions() deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, varsFile, true, "") mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) @@ -120,8 +119,7 @@ func TestTerraformACIInvalidTfFile(t *testing.T) { var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner var mpfService *usecase.MPFService - initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} - permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} + initialPermissionsToAdd, permissionsToAddToResult := getTerraformE2EBootstrapPermissions() deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "") mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) @@ -161,8 +159,7 @@ func TestTerraformACIInvalidTfExec(t *testing.T) { var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner var mpfService *usecase.MPFService - initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} - permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} + initialPermissionsToAdd, permissionsToAddToResult := getTerraformE2EBootstrapPermissions() deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "") mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) diff --git a/e2eTests/e2eTerraformPermissions_test.go b/e2eTests/e2eTerraformPermissions_test.go new file mode 100644 index 0000000..e11c97b --- /dev/null +++ b/e2eTests/e2eTerraformPermissions_test.go @@ -0,0 +1,69 @@ +// MIT License +// +// Copyright (c) Microsoft Corporation. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package e2etests + +import ( + "slices" + "testing" +) + +const terraformProviderReadPermission = "Microsoft.Resources/subscriptions/providers/read" + +func getTerraformE2EBootstrapPermissions(additionalPermissions ...string) ([]string, []string) { + // AzureRM 3.x/4.x can list subscription providers while initializing its + // provider cache. Seed the permission so residual RBAC state cannot hide it. + initialPermissions := append([]string{ + "Microsoft.Resources/deployments/read", + "Microsoft.Resources/deployments/write", + terraformProviderReadPermission, + }, additionalPermissions...) + + return initialPermissions, slices.Clone(initialPermissions) +} + +func TestE2ETerraformBootstrapPermissions(t *testing.T) { + expected := []string{ + "Microsoft.Resources/deployments/read", + "Microsoft.Resources/deployments/write", + terraformProviderReadPermission, + "Microsoft.Test/widgets/read", + } + + initialPermissions, resultPermissions := getTerraformE2EBootstrapPermissions("Microsoft.Test/widgets/read") + if !slices.Equal(initialPermissions, expected) { + t.Fatalf("unexpected initial permissions: %v", initialPermissions) + } + if !slices.Equal(resultPermissions, expected) { + t.Fatalf("unexpected result permissions: %v", resultPermissions) + } + + initialPermissions[0] = "modified" + if resultPermissions[0] != expected[0] { + t.Fatal("initial and result permissions share a backing array") + } + + nextInitialPermissions, _ := getTerraformE2EBootstrapPermissions("Microsoft.Test/widgets/read") + if !slices.Equal(nextInitialPermissions, expected) { + t.Fatalf("helper result was mutated across calls: %v", nextInitialPermissions) + } +} diff --git a/e2eTests/e2eTerraformWithImportAndTargeting_test.go b/e2eTests/e2eTerraformWithImportAndTargeting_test.go index 00cf90b..18f1c50 100644 --- a/e2eTests/e2eTerraformWithImportAndTargeting_test.go +++ b/e2eTests/e2eTerraformWithImportAndTargeting_test.go @@ -73,8 +73,7 @@ func TestTerraformWithImport(t *testing.T) { var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner var mpfService *usecase.MPFService - initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} - permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} + initialPermissionsToAdd, permissionsToAddToResult := getTerraformE2EBootstrapPermissions() deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "") mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) @@ -84,7 +83,7 @@ func TestTerraformWithImport(t *testing.T) { } assert.NotEmpty(t, mpfResult.RequiredPermissions) - assert.Equal(t, 17, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) + assert.Equal(t, 18, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) } func TestTerraformWithTargetting(t *testing.T) { @@ -124,8 +123,7 @@ func TestTerraformWithTargetting(t *testing.T) { var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner var mpfService *usecase.MPFService - initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} - permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} + initialPermissionsToAdd, permissionsToAddToResult := getTerraformE2EBootstrapPermissions() deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "module.law") mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) @@ -135,5 +133,5 @@ func TestTerraformWithTargetting(t *testing.T) { } assert.NotEmpty(t, mpfResult.RequiredPermissions) - assert.Equal(t, 8, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) + assert.Equal(t, 9, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) } diff --git a/e2eTests/e2eTerraform_test.go b/e2eTests/e2eTerraform_test.go index f2484f7..4cd047a 100644 --- a/e2eTests/e2eTerraform_test.go +++ b/e2eTests/e2eTerraform_test.go @@ -104,8 +104,7 @@ func TestTerraformACI(t *testing.T) { var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner var mpfService *usecase.MPFService - initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} - permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} + initialPermissionsToAdd, permissionsToAddToResult := getTerraformE2EBootstrapPermissions() deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, varsFile, true, "") mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) @@ -115,7 +114,7 @@ func TestTerraformACI(t *testing.T) { } assert.NotEmpty(t, mpfResult.RequiredPermissions) - assert.Equal(t, 8, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) + assert.Equal(t, 9, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) } func TestTerraformACINoTfvarsFile(t *testing.T) { @@ -154,8 +153,7 @@ func TestTerraformACINoTfvarsFile(t *testing.T) { var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner var mpfService *usecase.MPFService - initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} - permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} + initialPermissionsToAdd, permissionsToAddToResult := getTerraformE2EBootstrapPermissions() deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "") mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) @@ -165,7 +163,7 @@ func TestTerraformACINoTfvarsFile(t *testing.T) { } assert.NotEmpty(t, mpfResult.RequiredPermissions) - assert.Equal(t, 5, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) + assert.Equal(t, 6, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) } func TestTerraformModuleTest(t *testing.T) { @@ -204,8 +202,7 @@ func TestTerraformModuleTest(t *testing.T) { var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner var mpfService *usecase.MPFService - initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} - permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} + initialPermissionsToAdd, permissionsToAddToResult := getTerraformE2EBootstrapPermissions() deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "") mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) @@ -219,6 +216,7 @@ func TestTerraformModuleTest(t *testing.T) { // Microsoft.OperationalInsights/workspaces/write // Microsoft.Resources/deployments/read // Microsoft.Resources/deployments/write + // Microsoft.Resources/subscriptions/providers/read // Microsoft.Resources/subscriptions/resourcegroups/delete // Microsoft.Resources/subscriptions/resourcegroups/read // Microsoft.Resources/subscriptions/resourcegroups/write @@ -226,7 +224,7 @@ func TestTerraformModuleTest(t *testing.T) { assert.NotEmpty(t, mpfResult.RequiredPermissions) perms := mpfResult.RequiredPermissions[mpfConfig.SubscriptionID] log.Infof("Found %d permissions: %v", len(perms), perms) - assert.Equal(t, 8, len(perms)) + assert.Equal(t, 9, len(perms)) } // @@ -338,11 +336,9 @@ func TestTerraformACIWithInitialPermissions(t *testing.T) { var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner var mpfService *usecase.MPFService - // Provide ALL expected permissions upfront - this should result in 0 iterations - // These are the 8 permissions required for the ACI sample (from TestTerraformACI) - initialPermissionsToAdd := []string{ - "Microsoft.Resources/deployments/read", - "Microsoft.Resources/deployments/write", + // Provide ALL expected permissions upfront - this should result in 0 iterations. + // These are the 9 permissions required for the ACI sample (from TestTerraformACI). + initialPermissionsToAdd, permissionsToAddToResult := getTerraformE2EBootstrapPermissions( // ACI permissions "Microsoft.ContainerInstance/containerGroups/read", "Microsoft.ContainerInstance/containerGroups/write", @@ -351,8 +347,7 @@ func TestTerraformACIWithInitialPermissions(t *testing.T) { "Microsoft.Resources/subscriptions/resourcegroups/read", "Microsoft.Resources/subscriptions/resourcegroups/write", "Microsoft.Resources/subscriptions/resourcegroups/delete", - } - permissionsToAddToResult := initialPermissionsToAdd + ) deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, varsFile, true, "") mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) @@ -369,6 +364,6 @@ func TestTerraformACIWithInitialPermissions(t *testing.T) { // in 0 iterations (no permission discovery needed) assert.Equal(t, 0, mpfResult.IterationCount, "Expected 0 iterations when all permissions are provided upfront") - // Verify we have all 8 expected permissions - assert.Equal(t, 8, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) + // Verify we have all 9 expected permissions + assert.Equal(t, 9, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) }