├── .devcontainer └── devcontainer.json ├── .env.sh ├── .gitignore ├── .goreleaser.yml ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── Readme.MD ├── SECURITY.md ├── SUPPORT.md ├── Taskfile.yml ├── cmd ├── armCmd.go ├── bicepCmd.go ├── main.go ├── rootCmd.go └── terraformCmd.go ├── docs ├── commandline-flags-and-env-variables.md ├── display-options.MD ├── images │ ├── arm-sequence.svg │ ├── mpf-flow.svg │ ├── mpf-key-interface.svg │ ├── overview.png │ └── terraform-sequence.svg ├── installation-and-quickstart.md ├── known-issues-and-workarounds.MD ├── mpf-design.md └── overview.drawio ├── e2eTests ├── e2eArmInvalid_test.go ├── e2eArm_test.go ├── e2eBicepInvalid_test.go ├── e2eBicepSubscriptionScoped_test.go ├── e2eBicep_test.go ├── e2eTerraformAuthPermissionMismatch_test.go ├── e2eTerraformInvalid_test.go ├── e2eTerraformWithImportAndTargeting_test.go └── e2eTerraform_test.go ├── go.mod ├── go.sum ├── pkg ├── domain │ ├── InvalidActionOrNotActionErrorParser.go │ ├── InvalidActionOrNotActionErrorParser_test.go │ ├── appendPermissionsForSpecialCases.go │ ├── appendPermissionsForSpecialCases_test.go │ ├── authorizationErrorParser.go │ ├── authorizationErrorParser_test.go │ ├── authorizationPermissionMismatchErrorParser.go │ ├── authorizationPermissionMismatchErrorParser_test.go │ ├── linkedAccessCheckFailedErrorParser.go │ ├── linkedAccessCheckFailedErrorParser_test.go │ ├── mpfConfig.go │ ├── mpfResultFilterSort.go │ └── mpfResultFilterSort_test.go ├── infrastructure │ ├── ARMTemplateShared │ │ ├── armTemplateShared.go │ │ └── armTemplateShared_test.go │ ├── authorizationCheckers │ │ ├── ARMTemplateDeployment │ │ │ └── armTemplateAuthorizationChecker.go │ │ ├── ARMTemplateWhatIf │ │ │ └── armTemplateWhatIfAuthorizationChecker.go │ │ └── terraform │ │ │ ├── fileManager.go │ │ │ ├── fileManager_test.go │ │ │ ├── resourceImportParser.go │ │ │ ├── resourceImportParser_test.go │ │ │ └── terraformAuthorizationChecker.go │ ├── azureAPI │ │ └── azureApiClient.go │ ├── mpfSharedUtils │ │ ├── json.go │ │ ├── json_test.go │ │ └── randomString.go │ ├── resourceGroupManager │ │ └── defaultResourceGroupManager.go │ └── spRoleAssignmentManager │ │ └── defaultSPRoleAssignmentManager.go ├── presentation │ ├── defaultFormatter.go │ ├── jsonFormatter.go │ └── resultPresenter.go └── usecase │ ├── deploymentAuthorizationCheckerCleaner.go │ ├── mpfService.go │ ├── resourceGroupManager.go │ └── servicePrincipalRoleAssignmentManager.go └── samples ├── bicep ├── aks-private-subnet-invalid-params.json ├── aks-private-subnet-params.json ├── aks-private-subnet.bicep ├── invalid-bicep.bicep ├── subscription-scope-create-rg-params.json └── subscription-scope-create-rg.bicep ├── templates ├── aks-invalid-params.json ├── aks-invalid-template.json ├── aks-parameters.json ├── aks-private-subnet-parameters.json ├── aks-private-subnet.json ├── aks.json ├── blank-params.json ├── blank-template.json ├── empty.json ├── multi-resource-parameters.json ├── multi-resource-template.json ├── subscription-scope-create-rg-params.json └── subscription-scope-create-rg.json └── terraform ├── aci ├── dev.vars.tfvars ├── main.tf ├── output.tf └── variables.tf ├── authorization-permission-mismatch └── main.tf ├── existing-resource-import ├── main.tf ├── outputs.tf ├── terraform.tf └── variables.tf ├── module-test-with-targetting ├── main.tf ├── modules │ └── law │ │ ├── law.tf │ │ ├── rg.tf │ │ └── variables.tf ├── terraform.tfvars └── variables.tf ├── module-test ├── main.tf ├── modules │ └── law │ │ ├── law.tf │ │ └── variables.tf ├── terraform.tfvars └── variables.tf ├── multi-resource ├── dev.vars.tfvars ├── main.tf └── variables.tf ├── rg-invalid-tf-file ├── main.tf ├── output.tf └── variables.tf ├── rg-invalid-tfvars ├── dev.vars.tfvars ├── main.tf ├── output.tf └── variables.tf └── rg-no-tfvars ├── main.tf ├── output.tf └── variables.tf /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dev Container", 3 | "image": "mcr.microsoft.com/vscode/devcontainers/base:ubuntu", 4 | "features": { 5 | "ghcr.io/devcontainers/features/azure-cli:latest": { 6 | "version": "2.69.0", 7 | "installBicep": true 8 | }, 9 | "ghcr.io/devcontainers/features/terraform:1": { 10 | "version": "1.10.5" 11 | }, 12 | "ghcr.io/devcontainers/features/go:1": { 13 | "version": "1.24.0" 14 | } 15 | }, 16 | "customizations": { 17 | "vscode": { 18 | "extensions": [ 19 | "ms-azuretools.vscode-bicep", 20 | "hashicorp.terraform" 21 | ] 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /.env.sh: -------------------------------------------------------------------------------- 1 | SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID" 2 | TENANT_ID="YOUR_TENANT_ID" 3 | # TEST_DEPLOYMENT_NAME_PFX="testDeploy" 4 | # TEST_DEPLOYMENT_RESOURCE_GROUP_NAME_PFX="testdeployrg" 5 | SP_CLIENT_ID="YOUR_SP_CLIENT_ID" 6 | SP_CLIENT_SECRET="YOUR_SP_CLIENT_SECRET" 7 | SP_OBJECT_ID="YOUR_SP_OBJECT_ID" 8 | TEMPLATE_FILE="./templates/samples/aks.json" 9 | PARAMETERS_FILE="./templates/samples/aks-parameters.json" 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | commands.sh 2 | templates/template1/parameters copy.json 3 | addcustomrole.json 4 | .env.export.sh 5 | az-mpf 6 | az-mpf-darwin-amd64 7 | az-mpf-darwin-arm64 8 | az-mpf-linux-amd64 9 | az-mpf-windows-amd64.exe 10 | .vscode/launch-local.json 11 | 12 | **/.terraform/ 13 | **/*.tfstate 14 | **/*.tfstate.* 15 | **/*.hcl 16 | **/*.log 17 | 18 | coverage.* 19 | 20 | **/.DS_Store 21 | .lycheecache 22 | 23 | .gpg/ 24 | .task/ 25 | bin/ 26 | dist/ 27 | build/ 28 | *.exe 29 | .dev/ 30 | _dev/ 31 | __debug* 32 | *.env 33 | 34 | # Terraform 35 | .terraform/ 36 | *.tfplan 37 | *.tfstate 38 | *.tfstate.backup 39 | *.lock.hcl 40 | *.lock.info 41 | *.tfrc 42 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | # Visit https://goreleaser.com for documentation on how to customize this behavior. 3 | --- 4 | version: 2 5 | 6 | before: 7 | hooks: 8 | - go mod download 9 | 10 | archives: 11 | - files: 12 | - src: LICENSE 13 | dst: LICENSE.txt 14 | formats: [ 'zip' ] 15 | name_template: "az{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 16 | 17 | builds: 18 | - dir: cmd 19 | env: 20 | - CGO_ENABLED=0 21 | mod_timestamp: "{{ .CommitTimestamp }}" 22 | flags: 23 | - -trimpath 24 | ldflags: 25 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} 26 | goos: 27 | - windows 28 | - linux 29 | - darwin 30 | goarch: 31 | - amd64 32 | - "386" 33 | - arm 34 | - arm64 35 | ignore: 36 | - goos: darwin 37 | goarch: "386" 38 | binary: "az{{ .ProjectName }}_v{{ .Version }}" 39 | 40 | checksum: 41 | algorithm: sha256 42 | name_template: "az{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" 43 | 44 | signs: 45 | - artifacts: checksum 46 | args: 47 | # if you are using this in a GitHub action or some other automated pipeline, you 48 | # need to pass the batch flag to indicate its not interactive. 49 | - "--batch" 50 | - "--local-user" 51 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 52 | - "--output" 53 | - "${signature}" 54 | - "--detach-sign" 55 | - "${artifact}" 56 | 57 | release: 58 | # If you want to manually examine the release before its live, uncomment this line: 59 | draft: true 60 | prerelease: auto 61 | 62 | changelog: 63 | disable: false 64 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "debug", 12 | "program": "${workspaceFolder}/cmd/main.go" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE 22 | 23 | # Go parameters 24 | GOCMD = go 25 | GOBUILD = $(GOCMD) build 26 | GOCLEAN = $(GOCMD) clean 27 | GOTEST = $(GOCMD) test 28 | GOGET = $(GOCMD) get 29 | 30 | # Name of the binary output 31 | BINARY_NAME = azmpf 32 | 33 | # Main source file 34 | # MAIN_FILE = main.go 35 | 36 | # Output directory for the binary 37 | OUTPUT_DIR = . 38 | 39 | all: clean build 40 | 41 | build: 42 | @echo "Building $(BINARY_NAME)..." 43 | @mkdir -p $(OUTPUT_DIR) 44 | # $(GOBUILD) -o $(OUTPUT_DIR)/$(BINARY_NAME) ./cmd 45 | $(GOBUILD) -ldflags "-X 'main.version=$(shell git describe --tags --always --dirty)' -X 'main.commit=$(shell git rev-parse --short HEAD)' -X 'main.date=$(shell date -u '+%Y-%m-%d %H:%M:%S')'" -o $(OUTPUT_DIR)/$(BINARY_NAME) ./cmd 46 | 47 | 48 | 49 | test: 50 | @echo "Running tests..." 51 | $(GOTEST) -count=1 -v ./pkg/domain ./pkg/infrastructure/ARMTemplateShared ./pkg/infrastructure/mpfSharedUtils ./pkg/infrastructure/authorizationCheckers/terraform 52 | 53 | clean: 54 | @echo "Cleaning..." 55 | $(GOCLEAN) 56 | @rm -rf $(OUTPUT_DIR) 57 | 58 | run: build 59 | @echo "Running $(BINARY_NAME)..." 60 | @./$(OUTPUT_DIR)/$(BINARY_NAME) 61 | 62 | deps: 63 | @echo "Fetching dependencies..." 64 | $(GOGET) ./... 65 | 66 | .PHONY: all build test clean run deps 67 | 68 | # build for darwin arm64, darwin amd64, linux amd64, and windows amd64 69 | build-all: 70 | @echo "Building $(BINARY_NAME) for all target platforms..." 71 | @mkdir -p $(OUTPUT_DIR) 72 | GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(OUTPUT_DIR)/$(BINARY_NAME)-darwin-arm64 ./cmd 73 | GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(OUTPUT_DIR)/$(BINARY_NAME)-darwin-amd64 ./cmd 74 | GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(OUTPUT_DIR)/$(BINARY_NAME)-linux-amd64 ./cmd 75 | GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(OUTPUT_DIR)/$(BINARY_NAME)-windows-amd64.exe ./cmd 76 | 77 | test-e2e-arm: # arm and bicep tests 78 | @echo "Running end-to-end tests..." 79 | $(GOTEST) ./e2eTests -v -run TestARM 80 | 81 | test-e2e-bicep: # bicep tests 82 | @echo "Running end-to-end tests..." 83 | $(GOTEST) ./e2eTests -v -run TestBicep 84 | 85 | test-e2e-terraform: # terraform tests 86 | @echo "Running end-to-end tests..." 87 | $(GOTEST) ./e2eTests -v -timeout 20m -run TestTerraform 88 | 89 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | 2 | # Support 3 | 4 | ## How to use mpf 5 | 6 | For more information on how to use mpf please refer [README.MD](./README.md) 7 | 8 | ## How to file issues and get help 9 | 10 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 11 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 12 | feature request as a new Issue. 13 | 14 | ## Microsoft Support Policy 15 | 16 | Support for mpf is limited to the resources listed above. -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package main 24 | 25 | import ( 26 | "fmt" 27 | "os" 28 | 29 | log "github.com/sirupsen/logrus" 30 | ) 31 | 32 | var ( 33 | version = "dev" 34 | commit = "none" 35 | date = "unknown" 36 | ) 37 | 38 | func main() { 39 | 40 | logLevel, err := log.ParseLevel(os.Getenv("LOG_LEVEL")) 41 | if err != nil { 42 | logLevel = log.ErrorLevel 43 | } 44 | log.SetLevel(logLevel) 45 | 46 | rootCmd := NewRootCommand() 47 | rootCmd.Version = fmt.Sprintf(": %s, commit: %s, date: %s", version, commit, date) 48 | if err := rootCmd.Execute(); err != nil { 49 | os.Exit(1) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /cmd/terraformCmd.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package main 24 | 25 | import ( 26 | "context" 27 | "fmt" 28 | "os" 29 | 30 | "github.com/aliveticket/mpf/pkg/infrastructure/authorizationCheckers/terraform" 31 | resourceGroupManager "github.com/aliveticket/mpf/pkg/infrastructure/resourceGroupManager" 32 | sproleassignmentmanager "github.com/aliveticket/mpf/pkg/infrastructure/spRoleAssignmentManager" 33 | "github.com/aliveticket/mpf/pkg/usecase" 34 | log "github.com/sirupsen/logrus" 35 | 36 | "github.com/spf13/cobra" 37 | ) 38 | 39 | var ( 40 | flgTFPath string 41 | flgWorkingDir string 42 | flgVarFilePath string 43 | flgImportExistingResourcesToState bool 44 | flgTargetModule string 45 | ) 46 | 47 | const FoundPermissionsFromFailedRunFilename = ".permissionsFromFailedRun.json" 48 | 49 | // terraformCmd represents the terraform command 50 | // var 51 | 52 | func NewTerraformCommand() *cobra.Command { 53 | terraformCmd := &cobra.Command{ 54 | Use: "terraform", 55 | Short: "A brief description of your command", 56 | Long: `A longer description that spans multiple lines and likely contains examples 57 | and usage of using your command. For example: 58 | 59 | Cobra is a CLI library for Go that empowers applications. 60 | This application is a tool to generate the needed files 61 | to quickly create a Cobra application.`, 62 | Run: getMPFTerraform, 63 | } 64 | 65 | terraformCmd.Flags().StringVarP(&flgTFPath, "tfPath", "", "", "Path to Terraform Executable") 66 | err := terraformCmd.MarkFlagRequired("tfPath") 67 | if err != nil { 68 | log.Errorf("Error marking flag required for Terraform executable path: %v\n", err) 69 | } 70 | 71 | terraformCmd.Flags().StringVarP(&flgWorkingDir, "workingDir", "", "", "Path to Terraform Working Directory") 72 | err = terraformCmd.MarkFlagRequired("workingDir") 73 | if err != nil { 74 | log.Errorf("Error marking flag required for Terraform working directory: %v\n", err) 75 | } 76 | 77 | terraformCmd.Flags().StringVarP(&flgVarFilePath, "varFilePath", "", "", "Path to Terraform Variable File") 78 | terraformCmd.Flags().BoolVarP(&flgImportExistingResourcesToState, "importExistingResourcesToState", "", true, "On existing resource error, import existing resources into to Terraform State. This will also destroy the imported resources before MPF execution completes.") 79 | 80 | terraformCmd.Flags().StringVarP(&flgTargetModule, "targetModule", "", "", "The Terraform module to Target Module to run MPF on") 81 | 82 | return terraformCmd 83 | } 84 | 85 | func getMPFTerraform(cmd *cobra.Command, args []string) { 86 | setLogLevel() 87 | 88 | log.Info("Executin MPF for Terraform") 89 | log.Infof("TFPath: %s\n", flgTFPath) 90 | log.Infof("WorkingDir: %s\n", flgWorkingDir) 91 | log.Infof("VarFilePath: %s\n", flgVarFilePath) 92 | log.Infof("ImportExistingResourcesToState: %t\n", flgImportExistingResourcesToState) 93 | 94 | // validate if working directory exists 95 | if _, err := os.Stat(flgWorkingDir); os.IsNotExist(err) { 96 | log.Fatalf("Working Directory does not exist: %s\n", flgWorkingDir) 97 | } 98 | 99 | flgWorkingDir, err := getAbsolutePath(flgWorkingDir) 100 | if err != nil { 101 | log.Errorf("Error getting absolute path for terraform working directory: %v\n", err) 102 | } 103 | 104 | // validate if tfPath exists 105 | if _, err := os.Stat(flgTFPath); os.IsNotExist(err) { 106 | log.Fatalf("Terraform Executable does not exist: %s\n", flgTFPath) 107 | } 108 | 109 | flgTFPath, err := getAbsolutePath(flgTFPath) 110 | if err != nil { 111 | log.Errorf("Error getting absolute path for terraform executable: %v\n", err) 112 | } 113 | 114 | if flgVarFilePath != "" { 115 | 116 | if _, err := os.Stat(flgVarFilePath); os.IsNotExist(err) { 117 | log.Fatalf("Terraform Variable File does not exist: %s\n", flgVarFilePath) 118 | } 119 | 120 | flgVarFilePath, err = getAbsolutePath(flgVarFilePath) 121 | if err != nil { 122 | log.Errorf("Error getting absolute path for terraform variable file: %v\n", err) 123 | } 124 | 125 | } 126 | 127 | ctx := context.Background() 128 | 129 | mpfConfig := getRootMPFConfig() 130 | 131 | var rgManager usecase.ResourceGroupManager 132 | var spRoleAssignmentManager usecase.ServicePrincipalRolemAssignmentManager 133 | rgManager = resourceGroupManager.NewResourceGroupManager(flgSubscriptionID) 134 | spRoleAssignmentManager = sproleassignmentmanager.NewSPRoleAssignmentManager(flgSubscriptionID) 135 | 136 | var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner 137 | var mpfService *usecase.MPFService 138 | 139 | initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 140 | permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 141 | 142 | // Check if permissions file from previous failed run exists 143 | if terraform.DoesTFFileExist(flgWorkingDir, FoundPermissionsFromFailedRunFilename) { 144 | prevResult, err := terraform.LoadMPFResultFromFile(flgWorkingDir, FoundPermissionsFromFailedRunFilename) 145 | if err != nil { 146 | log.Warnf("Error loading permissions from previous failed run: %v\n, continuing....", err) 147 | } 148 | prevRunFoundPermissions := prevResult.RequiredPermissions[""] 149 | if len(prevRunFoundPermissions) > 0 { 150 | log.Warnf("Found permissions from previous failed run: %v\n Adding the Permissions....", prevRunFoundPermissions) 151 | initialPermissionsToAdd = append(initialPermissionsToAdd, prevRunFoundPermissions...) 152 | permissionsToAddToResult = append(permissionsToAddToResult, prevRunFoundPermissions...) 153 | } 154 | } 155 | 156 | deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(flgWorkingDir, flgTFPath, flgVarFilePath, flgImportExistingResourcesToState, flgTargetModule) 157 | mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) 158 | 159 | displayOptions := getDislayOptions(flgShowDetailedOutput, flgJSONOutput, mpfConfig.SubscriptionID) 160 | 161 | mpfResult, err := mpfService.GetMinimumPermissionsRequired() 162 | if err != nil { 163 | if len(mpfResult.RequiredPermissions) > 0 { 164 | fmt.Println("Error occurred while getting minimum permissions required. However, some permissions were identified prior to the error.") 165 | _ = terraform.SaveMPFResultsToFile(flgWorkingDir, FoundPermissionsFromFailedRunFilename, mpfResult) 166 | 167 | displayResult(mpfResult, displayOptions) 168 | } 169 | log.Fatal(err) 170 | } 171 | 172 | if terraform.DoesTFFileExist(flgWorkingDir, FoundPermissionsFromFailedRunFilename) { 173 | _ = terraform.DeleteTFFile(flgWorkingDir, FoundPermissionsFromFailedRunFilename) 174 | } 175 | 176 | displayResult(mpfResult, displayOptions) 177 | 178 | } 179 | -------------------------------------------------------------------------------- /docs/commandline-flags-and-env-variables.md: -------------------------------------------------------------------------------- 1 | # MPF command line flags and environment variables 2 | 3 | ## Global Flags (Common to all providers) 4 | 5 | | Flag | Environment Variable | Required / Optional | Description | 6 | | ------------------ | -------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------- | 7 | | subscriptionID | MPF_SUBSCRIPTIONID | Required | | 8 | | tenantID | MPF_TENANTID | Required | | 9 | | spClientID | MPF_SPCLIENTID | Required | | 10 | | spObjectID | MPF_SPOBJECTID | Required | Note this is the SP Object id and is different from the Client ID | 11 | | spClientSecret | MPF_SPCLIENTSECRET | Required | | 12 | | showDetailedOutput | MPF_SHOWDETAILEDOUTPUT | Optional | If set to true, the output shows details of permissions resource wise as well. This is not needed if --jsonOutput is specified | 13 | | jsonOutput | MPF_JSONOUTPUT | Optional | If set to true, the detailed output is printed in JSON format | 14 | | verbose | MPF_VERBOSE | Optional | If set to true, verbose output with informational messages is displayed | 15 | | debug | MPF_DEBUG | Optional | If set to true, output with detailed debug messages is displayed. The debug messages may contain sensitive tokens | 16 | 17 | When used for Terraform, the verbose and debug flags show detailed logs from Terraform. 18 | 19 | ## ARM Flags 20 | 21 | | Flag | Environment Variable | Required / Optional | Description | 22 | | -------------------- | ------------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------- | 23 | | templateFilePath | MPF_TEMPLATEFILEPATH | Required | ARM template file with path | 24 | | parametersFilePath | MPF_PARAMETERSFILEPATH | Required | ARM template parameters file with path | 25 | | resourceGroupNamePfx | MPF_RESOURCEGROUPNAMEPFX | Optional | Prefix for the resource group name. If not provided, default prefix is testdeployrg. For ARM deployments this temporary resource group is created | 26 | | deploymentNamePfx | MPF_DEPLOYMENTNAMEPFX | Optional | Prefix for the deployment name. If not provided, default prefix is testDeploy. For ARM deployments this temporary deployment is created | 27 | | location | MPF_LOCATION | Optional | Location for the resource group. If not provided, default location is eastus | 28 | | subscriptionScoped | MPF_SUBSCRIPTIONSCOPED | Optional | This flag indicates whether the deployment is scoped to a subscription. If set, the deployment will target a subscription else it is resource group scoped deployment. | 29 | 30 | ### Bicep Flags 31 | 32 | | Flag | Environment Variable | Required / Optional | Description | 33 | | -------------------- | ------------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------- | 34 | | bicepFilePath | MPF_BICEPFILEPATH | Required | Bicep file with path | 35 | | parametersFilePath | MPF_PARAMETERSFILEPATH | Required | Bicep parameters file with path | 36 | | bicepExecPath | MPF_BICEPEXECPATH | Required | Path to the Bicep executable | 37 | | resourceGroupNamePfx | MPF_RESOURCEGROUPNAMEPFX | Optional | Prefix for the resource group name. If not provided, default prefix is testdeployrg. For Bicep deployments this temporary resource group is created | 38 | | deploymentNamePfx | MPF_DEPLOYMENTNAMEPFX | Optional | Prefix for the deployment name. If not provided, default prefix is testDeploy. For Bicep deployments this temporary deployment is created | 39 | | location | MPF_LOCATION | Optional | Location for the resource group. If not provided, default location is eastus | 40 | | subscriptionScoped | MPF_SUBSCRIPTIONSCOPED | Optional | This flag indicates whether the deployment is scoped to a subscription. If set, the deployment will target a subscription else it is resource group scoped deployment. | 41 | 42 | ## Terraform Flags 43 | 44 | | Flag | Environment Variable | Required / Optional | Description | 45 | | -------------------- | ------------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------- | 46 | | tfPath | MPF_TFPATH | Required | Path to the Terraform executable | 47 | | workingDir | MPF_WORKINGDIR | Required | Path to the Terraform module directory | 48 | | varFilePath | MPF_VARFILEPATH | Optional | Path to the Terraform variables file | 49 | | importExistingResourcesToState | MPF_IMPORTEXISTINGRESOURCESTOSTATE | Optional | Default Value is true. This is required for some scenarios as described in the [Known Issues - Import Errors](./known-issues-and-workarounds.MD#existing-resource--import-errors) | 50 | | targetModule | MPF_TARGETMODULE | Optional | Target module to be used for the Terraform deployment | 51 | -------------------------------------------------------------------------------- /docs/images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliveticket/mpf/333389be98abd73796561e623d301a095bd4a2cf/docs/images/overview.png -------------------------------------------------------------------------------- /docs/installation-and-quickstart.md: -------------------------------------------------------------------------------- 1 | # MPF Installation and Quickstart 2 | 3 | ## Installation 4 | 5 | You can download the latest version for your platform from the [releases](https://github.com/aliveticket/mpf/releases) link. 6 | 7 | For example, to download the latest version for Linux/amd64: 8 | 9 | ```shell 10 | # Please change the version in the URL to the latest version 11 | curl -LO https://github.com/aliveticket/mpf/releases/download/v0.14.0/azmpf_0.14.0_linux_amd64.zip 12 | unzip azmpf_0.14.0_linux_amd64.zip 13 | mv azmpf_v0.14.0 azmpf 14 | chmod +x ./azmpf 15 | ``` 16 | 17 | And for Mac Arm64: 18 | 19 | ```shell 20 | # Please change the version in the URL to the latest version 21 | curl -LO https://github.com/aliveticket/mpf/releases/download/v0.14.0/azmpf_0.14.0_darwin_arm64.zip 22 | unzip azmpf_0.14.0_darwin_arm64.zip 23 | mv azmpf_v0.14.0 azmpf 24 | chmod +x ./azmpf 25 | ``` 26 | 27 | ## Creating a service principal for MPF 28 | 29 | To use MPF, you need to create a service principal in your Azure Active Directory tenant. You can create a service principal using the Azure CLI or the Azure portal. The service principal needs no roles assigned to it, as the MPF utility will as it is remove any assigned roles each time it executes. 30 | Here is an example of how to create a service principal using the Azure CLI: 31 | 32 | ```shell 33 | # az login 34 | 35 | MPF_SP=$(az ad sp create-for-rbac --name "MPF_SP" --skip-assignment) 36 | MPF_SPCLIENTID=$(echo $MPF_SP | jq -r .appId) 37 | MPF_SPCLIENTSECRET=$(echo $MPF_SP | jq -r .password) 38 | MPF_SPOBJECTID=$(az ad sp show --id $MPF_SPCLIENTID --query id -o tsv) 39 | ``` 40 | 41 | ## Quickstart / Usage 42 | 43 | ### ARM 44 | 45 | ```shell 46 | export MPF_SUBSCRIPTIONID=YOUR_SUBSCRIPTION_ID 47 | export MPF_TENANTID=YOUR_TENANT_ID 48 | export MPF_SPCLIENTID=YOUR_SP_CLIENT_ID 49 | export MPF_SPCLIENTSECRET=YOUR_SP_CLIENT_SECRET 50 | export MPF_SPOBJECTID=YOUR_SP_OBJECT_ID 51 | 52 | $ ./azmpf arm --templateFilePath ./samples/templates/aks-private-subnet.json --parametersFilePath ./samples/templates/aks-private-subnet-parameters.json 53 | 54 | ------------------------------------------------------------------------------------------------------------------------------------------ 55 | Permissions Required: 56 | ------------------------------------------------------------------------------------------------------------------------------------------ 57 | Microsoft.ContainerService/managedClusters/read 58 | Microsoft.ContainerService/managedClusters/write 59 | Microsoft.Network/virtualNetworks/read 60 | Microsoft.Network/virtualNetworks/subnets/read 61 | Microsoft.Network/virtualNetworks/subnets/write 62 | Microsoft.Network/virtualNetworks/write 63 | Microsoft.Resources/deployments/read 64 | Microsoft.Resources/deployments/write 65 | 66 | ``` 67 | 68 | ### Bicep 69 | 70 | ```shell 71 | export MPF_SUBSCRIPTIONID=YOUR_SUBSCRIPTION_ID 72 | export MPF_TENANTID=YOUR_TENANT_ID 73 | export MPF_SPCLIENTID=YOUR_SP_CLIENT_ID 74 | export MPF_SPCLIENTSECRET=YOUR_SP_CLIENT_SECRET 75 | export MPF_SPOBJECTID=YOUR_SP_OBJECT_ID 76 | export MPF_BICEPEXECPATH="/opt/homebrew/bin/bicep" # Path to the Bicep executable 77 | 78 | $ ./azmpf bicep --bicepFilePath ./samples/bicep/aks-private-subnet.bicep --parametersFilePath ./samples/bicep/aks-private-subnet-params.json 79 | 80 | ------------------------------------------------------------------------------------------------------------------------------------------ 81 | Permissions Required: 82 | ------------------------------------------------------------------------------------------------------------------------------------------ 83 | Microsoft.ContainerService/managedClusters/read 84 | Microsoft.ContainerService/managedClusters/write 85 | Microsoft.Network/virtualNetworks/read 86 | Microsoft.Network/virtualNetworks/subnets/read 87 | Microsoft.Network/virtualNetworks/subnets/write 88 | Microsoft.Network/virtualNetworks/write 89 | Microsoft.Resources/deployments/read 90 | Microsoft.Resources/deployments/write 91 | ------------------------------------------------------------------------------------------------------------------------------------------ 92 | 93 | ``` 94 | 95 | ### Terraform 96 | 97 | ```shell 98 | export MPF_SUBSCRIPTIONID=YOUR_SUBSCRIPTION_ID 99 | export MPF_TENANTID=YOUR_TENANT_ID 100 | export MPF_SPCLIENTID=YOUR_SP_CLIENT_ID 101 | export MPF_SPCLIENTSECRET=YOUR_SP_CLIENT_SECRET 102 | export MPF_SPOBJECTID=YOUR_SP_OBJECT_ID 103 | export MPF_TFPATH=TERRAFORM_EXECUTABLE_PATH 104 | 105 | # pushd . 106 | # cd ./samples/terraform/aci/ 107 | # $MPF_TFPATH init 108 | # popd 109 | 110 | $ ./azmpf terraform --workingDir `pwd`/samples/terraform/aci --varFilePath `pwd`/samples/terraform/aci/dev.vars.tfvars --debug 111 | . 112 | # debug information 113 | . 114 | ------------------------------------------------------------------------------------------------------------------------------------------ 115 | Permissions Required: 116 | ------------------------------------------------------------------------------------------------------------------------------------------ 117 | Microsoft.ContainerInstance/containerGroups/delete 118 | Microsoft.ContainerInstance/containerGroups/read 119 | Microsoft.ContainerInstance/containerGroups/write 120 | Microsoft.Resources/deployments/read 121 | Microsoft.Resources/deployments/write 122 | Microsoft.Resources/subscriptions/resourcegroups/delete 123 | Microsoft.Resources/subscriptions/resourcegroups/read 124 | Microsoft.Resources/subscriptions/resourcegroups/write 125 | ------------------------------------------------------------------------------------------------------------------------------------------ 126 | 127 | ``` 128 | 129 | It is also possible to additionally view detailed resource-level permissions required as shown in the [display options](./display-options.MD) document. 130 | 131 | The blog post [Figuring out the Minimum Permissions Required to Deploy an Azure ARM Template](https://medium.com/microsoftazure/figuring-out-the-minimum-permissions-required-to-deploy-an-azure-arm-template-d1c1e74092fa) provides a more contextual usage scenario for azmpf. 132 | -------------------------------------------------------------------------------- /docs/known-issues-and-workarounds.MD: -------------------------------------------------------------------------------- 1 | # Known Issues and Workarounds 2 | 3 | ## Bicep 4 | 5 | ### Parameter File Format 6 | 7 | Currently only ARM type [parameters files](https://github.com/aliveticket/mpf/blob/main/samples/bicep/aks-private-subnet-invalid-params.json) are supported even for bicep executions. Bicep type parameters files are currently not supported and result in an error. A new feature request has been created to track this issue [issue #12](https://github.com/aliveticket/mpf/issues/12). 8 | 9 | ## Terraform 10 | 11 | ### Token Expiry 12 | 13 | If your default Azure credentials token issued for the utility expires before the utility completes the execution, that execution will fail. When this happens, the utility saves the permissions inferred up to that point in the Terraform module directory, and these are automatically added next time the utility executes for the same Terraform module directory. 14 | 15 | ### Terraform azurerm provider crash 16 | 17 | The azurerm provider version < 4.2.0 can crash when using resources like `azurerm_application_insights` without all permissions to create the resource. Details can be found in the [GitHub issue](https://github.com/hashicorp/terraform-provider-azurerm/issues/27961#issuecomment-2467155082). If this issue is encountered, it is recommended to upgrade your azurerm provider version to 4.2.0 or higher. It must be noted that there are some Azure Verified Modules which still use older versions of the Azure provider. 18 | 19 | ### Existing Resource / Import Errors 20 | 21 | Creation of certain resources, like the application insights resource, also involves the creation of current billing features resource, as described in the [GitHub issue](https://github.com/hashicorp/terraform-provider-azurerm/issues/27961#issuecomment-2467392936). This means that if the identity used by Terraform has permissions to create the application insights resource but not the current billing features resource, the application insights resource will be created in Azure, but the Terraform apply will fail. This means that the creation of the application insights resource will not be tracked by Terraform and the state file will be out of sync with the actual resources in Azure. When the utility adds the required permission and executes Terraform apply again, Terraform will give an error that the resource already exists in Azure and that this can be resolved by importing the resource into the Terraform state file. As a workaround, the utility automatically appends [missing required permissions](../pkg/domain/appendPermissionsForSpecialCases.go#L12-19) when ```Microsoft.Insights/components/read``` or ```Microsoft.Insights/components/write``` permissions are detected as missing permissions. 22 | When ```Microsoft.Insights/components/read``` is detected ```Microsoft.Insights/components/currentbillingfeatures/read``` and ```Microsoft.AlertsManagement/smartDetectorAlertRules/read``` permissions are appended. Similarly, when ```Microsoft.Insights/components/write``` is detected ```Microsoft.Insights/components/currentbillingfeatures/write``` and ```Microsoft.AlertsManagement/smartDetectorAlertRules/write``` permissions are appended. 23 | 24 | 25 | ### Billing Features Payload Error 26 | 27 | This issue is also related to the [GitHub Issue](https://github.com/hashicorp/terraform-provider-azurerm/issues/27961#issuecomment-2520407658). The utility retries the request to work around this issue. 28 | 29 | ### Authorization_RequestDenied Error 30 | 31 | Currently if you attempt perform actions like adding an Azure AD group via terraform, a Authorization_RequestDenied Error is received. 32 | 33 | Sample Error: 34 | 35 | ``` 36 | Error: Creating group "Group-name-axtwb" 37 | 38 | with ...._ds_group[0], 39 | on ....../rbac.tf line 3, in resource "azuread_group" "res_ds_group": 40 | 3: resource "azuread_group" "res_ds_group" { 41 | 42 | GroupsClient.BaseClient.Post(): unexpected status 403 with OData error: 43 | Authorization_RequestDenied: Insufficient privileges to complete the ... 44 | ``` 45 | 46 | From the [terraform docs](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/guides/service_principal_configuration), adding these permissions may require global admin privilege or admin consent. For this reason the utility cannot automatically add any permissions to the MPF SP to get around this error. 47 | 48 | A potential workaround is to disable the Azure AD resource creation which caused this error in the terraform code, and then re-execute the utility. 49 | 50 | ## Common 51 | 52 | ### Execution Time Reduction Workarounds 53 | 54 | For ARM and Bicep, autoAddReadPermissionForEachWrite is set to true by default. This means that if a write permission is detected, the utility will automatically add the corresponding read permission. 55 | 56 | For Terraform autoAddDeletePermissionForEachWrite is set to true by default. This means that if a write permission is detected, the utility will automatically add the corresponding delete permission. -------------------------------------------------------------------------------- /docs/mpf-design.md: -------------------------------------------------------------------------------- 1 | # Minimum Permissions Finder (MPF) Design 2 | 3 | The [High Level MPF flow](../Readme.MD#how-it-works) describes the overall flow of the Minimum Permissions Finder (MPF) system. This document provides a detailed design of the MPF system, including the key components and abstractions. 4 | 5 | ## Key Packages and Abstractions 6 | 7 | ![Key Interfaces and Packages](./images/mpf-key-interface.svg) 8 | 9 | For each deployment type (ARM, Terraform) an implementation of the `DeploymentAuthorizationCheckerCleaner` interface is provided. The two key methods which need to be implemented for each deployment type implementation are GetDeploymentAuthorizationErrors() and CleanUpResources(). 10 | 11 | * The deployment type commands i.e. [armCmd](../cmd/armCmd.go), [bicepCmd](../cmd/bicepCmd.go), and [terraformCmd](../cmd/terraformCmd.go) are responsible for initializing the required dependencies including the `MPFService` to find the minimum permissions required for the deployment. This is illustrated in the sequence diagram below. 12 | * [pkg/usecase/mpfService.go](../pkg/usecase/mpfService.go): Orchestrates the whole process of finding the minimum permissions required for any deployment type (ARM/Bicep/Terraform). It uses the `DeploymentAuthorizationCheckerCleaner` abstraction for any deployment type, be it ARM, Bicep, or Terraform. On receiving deployment authorization errors, it uses the `AuthorizationErrorParser` to parse the authorization errors and get the missing permissions and scopes. After adding the missing permissions to the custom role, it retries the deployment until it succeeds. It also cleans up all resources created during the process. 13 | * [pkg/infrastructure/authorizationCheckers/ARMTemplateWhatIf/armTemplateWhatIfAuthorizationChecker.go](../pkg/infrastructure/authorizationCheckers/ARMTemplateWhatIf/armTemplateWhatIfAuthorizationChecker.go): Contains the DeploymentAuthorizationCheckerCleaner implementation for ARM (and Bicep) deployments. 14 | * [pkg/infrastructure/authorizationCheckers/terraform/terraformAuthorizationChecker.go](../pkg/infrastructure/authorizationCheckers/terraform/terraformAuthorizationChecker.go): Contains the DeploymentAuthorizationCheckerCleaner implementation for Terraform deployments. 15 | * [pkg/domain/authorizationErrorParser.go](../pkg/domain/authorizationErrorParser.go): Contains the core logic for the MPF, which is to parse the different kinds of authorization errors and figure out the required permissions and scopes from those errors. 16 | 17 | ## ARM and Terraform Sequence Diagrams 18 | 19 | ### ARM Sequence Diagram 20 | 21 | For Bicep, the only difference with the ARM template flow is that as a first step the bicepCmd converts the Bicep file to an ARM template file. 22 | 23 | ![ARM Sequence Diagram](./images/arm-sequence.svg) 24 | 25 | ### Terraform Sequence Diagram 26 | 27 | ![Terraform Sequence Diagram](./images/terraform-sequence.svg) 28 | -------------------------------------------------------------------------------- /e2eTests/e2eBicepInvalid_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package e2etests 24 | 25 | import ( 26 | "errors" 27 | "fmt" 28 | "os" 29 | "os/exec" 30 | "path/filepath" 31 | "strings" 32 | "testing" 33 | 34 | "github.com/aliveticket/mpf/pkg/infrastructure/ARMTemplateShared" 35 | "github.com/aliveticket/mpf/pkg/infrastructure/authorizationCheckers/ARMTemplateWhatIf" 36 | mpfSharedUtils "github.com/aliveticket/mpf/pkg/infrastructure/mpfSharedUtils" 37 | rgm "github.com/aliveticket/mpf/pkg/infrastructure/resourceGroupManager" 38 | spram "github.com/aliveticket/mpf/pkg/infrastructure/spRoleAssignmentManager" 39 | "github.com/aliveticket/mpf/pkg/usecase" 40 | log "github.com/sirupsen/logrus" 41 | "github.com/stretchr/testify/assert" 42 | ) 43 | 44 | func TestBicepInvalidParams(t *testing.T) { 45 | 46 | mpfArgs, err := getTestingMPFArgs() 47 | if err != nil { 48 | t.Skip("required environment variables not set, skipping end to end test") 49 | } 50 | 51 | if checkBicepTestEnvVars() { 52 | t.Skip("required environment variables not set, skipping end to end test") 53 | } 54 | 55 | bicepExecPath := os.Getenv("MPF_BICEPEXECPATH") 56 | bicepFilePath := "../samples/bicep/aks-private-subnet.bicep" 57 | parametersFilePath := "../samples/bicep/aks-invalid-params.json" 58 | 59 | bicepFilePath, _ = getAbsolutePath(bicepFilePath) 60 | parametersFilePath, _ = getAbsolutePath(parametersFilePath) 61 | 62 | armTemplatePath := strings.TrimSuffix(bicepFilePath, ".bicep") + ".json" 63 | bicepCmd := exec.Command(bicepExecPath, "build", bicepFilePath, "--outfile", armTemplatePath) 64 | bicepCmd.Dir = filepath.Dir(bicepFilePath) 65 | 66 | _, err = bicepCmd.CombinedOutput() 67 | if err != nil { 68 | log.Error(err) 69 | t.Error(err) 70 | } 71 | // defer os.Remove(armTemplatePath) 72 | 73 | ctx := t.Context() 74 | 75 | mpfConfig := getMPFConfig(mpfArgs) 76 | 77 | deploymentName := fmt.Sprintf("%s-%s", mpfArgs.DeploymentNamePfx, mpfSharedUtils.GenerateRandomString(7)) 78 | armConfig := &ARMTemplateShared.ArmTemplateAdditionalConfig{ 79 | TemplateFilePath: armTemplatePath, 80 | ParametersFilePath: parametersFilePath, 81 | DeploymentName: deploymentName, 82 | } 83 | 84 | // azAPIClient := azureAPI.NewAzureAPIClients(mpfArgs.SubscriptionID) 85 | var rgManager usecase.ResourceGroupManager 86 | var spRoleAssignmentManager usecase.ServicePrincipalRolemAssignmentManager 87 | rgManager = rgm.NewResourceGroupManager(mpfArgs.SubscriptionID) 88 | spRoleAssignmentManager = spram.NewSPRoleAssignmentManager(mpfArgs.SubscriptionID) 89 | 90 | var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner 91 | var mpfService *usecase.MPFService 92 | 93 | deploymentAuthorizationCheckerCleaner = ARMTemplateWhatIf.NewARMTemplateWhatIfAuthorizationChecker(mpfArgs.SubscriptionID, *armConfig) 94 | initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/*", "Microsoft.Resources/subscriptions/operationresults/read"} 95 | permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 96 | mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, true, false, true) 97 | 98 | _, err = mpfService.GetMinimumPermissionsRequired() 99 | assert.Error(t, err) 100 | 101 | if !errors.Is(err, ARMTemplateShared.ErrInvalidTemplate) { 102 | t.Errorf("Error is not of type InvalidTemplate") 103 | } 104 | } 105 | 106 | func TestBicepInvalidResourceFile(t *testing.T) { 107 | 108 | _, err := getTestingMPFArgs() 109 | if err != nil { 110 | t.Skip("required environment variables not set, skipping end to end test") 111 | } 112 | 113 | if checkBicepTestEnvVars() { 114 | t.Skip("required environment variables not set, skipping end to end test") 115 | } 116 | 117 | bicepExecPath := os.Getenv("MPF_BICEPEXECPATH") 118 | bicepFilePath := "../samples/bicep/invalid-bicep.bicep" 119 | 120 | bicepFilePath, err = getAbsolutePath(bicepFilePath) 121 | if err != nil { 122 | t.Error(err) 123 | } 124 | 125 | armTemplatePath := strings.TrimSuffix(bicepFilePath, ".bicep") + ".json" 126 | bicepCmd := exec.Command(bicepExecPath, "build", bicepFilePath, "--outfile", armTemplatePath) 127 | bicepCmd.Dir = filepath.Dir(bicepFilePath) 128 | 129 | _, err = bicepCmd.CombinedOutput() 130 | if err == nil { 131 | // log.Error(err) 132 | t.Error("expected error, got nil") 133 | } 134 | // defer os.Remove(armTemplatePath) 135 | 136 | } 137 | -------------------------------------------------------------------------------- /e2eTests/e2eBicepSubscriptionScoped_test.go: -------------------------------------------------------------------------------- 1 | package e2etests 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/aliveticket/mpf/pkg/infrastructure/ARMTemplateShared" 12 | "github.com/aliveticket/mpf/pkg/infrastructure/authorizationCheckers/ARMTemplateWhatIf" 13 | mpfSharedUtils "github.com/aliveticket/mpf/pkg/infrastructure/mpfSharedUtils" 14 | spram "github.com/aliveticket/mpf/pkg/infrastructure/spRoleAssignmentManager" 15 | "github.com/aliveticket/mpf/pkg/usecase" 16 | log "github.com/sirupsen/logrus" 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | func TestBicepSubscriptionScoped(t *testing.T) { 21 | 22 | mpfArgs, err := getTestingMPFArgs() 23 | if err != nil { 24 | t.Skip("required environment variables not set, skipping end to end test") 25 | } 26 | 27 | if checkBicepTestEnvVars() { 28 | t.Skip("required environment variables not set, skipping end to end test") 29 | } 30 | 31 | bicepExecPath := os.Getenv("MPF_BICEPEXECPATH") 32 | bicepFilePath := "../samples/bicep/subscription-scope-create-rg.bicep" 33 | parametersFilePath := "../samples/bicep/subscription-scope-create-rg-params.json" 34 | 35 | bicepFilePath, _ = getAbsolutePath(bicepFilePath) 36 | parametersFilePath, _ = getAbsolutePath(parametersFilePath) 37 | 38 | armTemplatePath := strings.TrimSuffix(bicepFilePath, ".bicep") + ".json" 39 | bicepCmd := exec.Command(bicepExecPath, "build", bicepFilePath, "--outfile", armTemplatePath) 40 | bicepCmd.Dir = filepath.Dir(bicepFilePath) 41 | 42 | _, err = bicepCmd.CombinedOutput() 43 | if err != nil { 44 | log.Error(err) 45 | t.Error(err) 46 | } 47 | // defer os.Remove(armTemplatePath) 48 | 49 | ctx := t.Context() 50 | 51 | mpfConfig := getMPFConfig(mpfArgs) 52 | 53 | deploymentName := fmt.Sprintf("%s-%s", mpfArgs.DeploymentNamePfx, mpfSharedUtils.GenerateRandomString(7)) 54 | armConfig := &ARMTemplateShared.ArmTemplateAdditionalConfig{ 55 | TemplateFilePath: armTemplatePath, 56 | ParametersFilePath: parametersFilePath, 57 | DeploymentName: deploymentName, 58 | SubscriptionScoped: true, 59 | Location: "eastus2", 60 | } 61 | 62 | spRoleAssignmentManager := spram.NewSPRoleAssignmentManager(mpfArgs.SubscriptionID) 63 | 64 | var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner 65 | var mpfService *usecase.MPFService 66 | 67 | deploymentAuthorizationCheckerCleaner = ARMTemplateWhatIf.NewARMTemplateWhatIfAuthorizationChecker(mpfArgs.SubscriptionID, *armConfig) 68 | initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/*", "Microsoft.Resources/subscriptions/operationresults/read"} 69 | permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 70 | mpfService = usecase.NewMPFService(ctx, nil, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, true, false, false) 71 | 72 | mpfResult, err := mpfService.GetMinimumPermissionsRequired() 73 | if err != nil { 74 | t.Error(err) 75 | } 76 | 77 | // Check if mpfResult.RequiredPermissions is not empty and has 2 permissions for scope SubscriptionID 78 | // Microsoft.Resources/deployments/read 79 | // Microsoft.Resources/deployments/write 80 | // Microsoft.Resources/subscriptions/resourceGroups/read 81 | // Microsoft.Resources/subscriptions/resourceGroups/write 82 | assert.NotEmpty(t, mpfResult.RequiredPermissions) 83 | assert.Equal(t, 4, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) 84 | } 85 | -------------------------------------------------------------------------------- /e2eTests/e2eBicep_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package e2etests 24 | 25 | import ( 26 | "fmt" 27 | "os" 28 | "os/exec" 29 | "path/filepath" 30 | "strings" 31 | "testing" 32 | 33 | "github.com/aliveticket/mpf/pkg/infrastructure/ARMTemplateShared" 34 | "github.com/aliveticket/mpf/pkg/infrastructure/authorizationCheckers/ARMTemplateWhatIf" 35 | mpfSharedUtils "github.com/aliveticket/mpf/pkg/infrastructure/mpfSharedUtils" 36 | rgm "github.com/aliveticket/mpf/pkg/infrastructure/resourceGroupManager" 37 | spram "github.com/aliveticket/mpf/pkg/infrastructure/spRoleAssignmentManager" 38 | "github.com/aliveticket/mpf/pkg/usecase" 39 | log "github.com/sirupsen/logrus" 40 | "github.com/stretchr/testify/assert" 41 | ) 42 | 43 | func checkBicepTestEnvVars() bool { 44 | return os.Getenv("MPF_BICEPEXECPATH") == "" 45 | } 46 | 47 | func TestBicepAks(t *testing.T) { 48 | 49 | mpfArgs, err := getTestingMPFArgs() 50 | if err != nil { 51 | t.Skip("required environment variables not set, skipping end to end test") 52 | } 53 | 54 | if checkBicepTestEnvVars() { 55 | t.Skip("required environment variables not set, skipping end to end test") 56 | } 57 | 58 | bicepExecPath := os.Getenv("MPF_BICEPEXECPATH") 59 | bicepFilePath := "../samples/bicep/aks-private-subnet.bicep" 60 | parametersFilePath := "../samples/bicep/aks-private-subnet-params.json" 61 | 62 | bicepFilePath, _ = getAbsolutePath(bicepFilePath) 63 | parametersFilePath, _ = getAbsolutePath(parametersFilePath) 64 | 65 | armTemplatePath := strings.TrimSuffix(bicepFilePath, ".bicep") + ".json" 66 | bicepCmd := exec.Command(bicepExecPath, "build", bicepFilePath, "--outfile", armTemplatePath) 67 | bicepCmd.Dir = filepath.Dir(bicepFilePath) 68 | 69 | _, err = bicepCmd.CombinedOutput() 70 | if err != nil { 71 | log.Error(err) 72 | t.Error(err) 73 | } 74 | // defer os.Remove(armTemplatePath) 75 | 76 | ctx := t.Context() 77 | 78 | mpfConfig := getMPFConfig(mpfArgs) 79 | 80 | deploymentName := fmt.Sprintf("%s-%s", mpfArgs.DeploymentNamePfx, mpfSharedUtils.GenerateRandomString(7)) 81 | armConfig := &ARMTemplateShared.ArmTemplateAdditionalConfig{ 82 | TemplateFilePath: armTemplatePath, 83 | ParametersFilePath: parametersFilePath, 84 | DeploymentName: deploymentName, 85 | } 86 | 87 | // azAPIClient := azureAPI.NewAzureAPIClients(mpfArgs.SubscriptionID) 88 | var rgManager usecase.ResourceGroupManager 89 | var spRoleAssignmentManager usecase.ServicePrincipalRolemAssignmentManager 90 | rgManager = rgm.NewResourceGroupManager(mpfArgs.SubscriptionID) 91 | spRoleAssignmentManager = spram.NewSPRoleAssignmentManager(mpfArgs.SubscriptionID) 92 | 93 | var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner 94 | var mpfService *usecase.MPFService 95 | 96 | deploymentAuthorizationCheckerCleaner = ARMTemplateWhatIf.NewARMTemplateWhatIfAuthorizationChecker(mpfArgs.SubscriptionID, *armConfig) 97 | initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/*", "Microsoft.Resources/subscriptions/operationresults/read"} 98 | permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 99 | mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, true, false, true) 100 | 101 | mpfResult, err := mpfService.GetMinimumPermissionsRequired() 102 | if err != nil { 103 | t.Error(err) 104 | } 105 | 106 | //check if mpfResult.RequiredPermissions is not empty and has 8 permissions for scope ResourceGroupResourceID 107 | // Microsoft.ContainerService/managedClusters/read 108 | // Microsoft.ContainerService/managedClusters/write 109 | // Microsoft.Network/virtualNetworks/read 110 | // Microsoft.Network/virtualNetworks/subnets/read 111 | // Microsoft.Network/virtualNetworks/subnets/write 112 | // Microsoft.Network/virtualNetworks/write 113 | // Microsoft.Resources/deployments/read 114 | // Microsoft.Resources/deployments/write 115 | assert.NotEmpty(t, mpfResult.RequiredPermissions) 116 | assert.Equal(t, 8, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) 117 | } 118 | 119 | func getAbsolutePath(path string) (string, error) { 120 | absPath := path 121 | if !filepath.IsAbs(path) { 122 | 123 | absWorkingDir, err := os.Getwd() 124 | if err != nil { 125 | return "", err 126 | } 127 | absPath = absWorkingDir + "/" + absPath 128 | } 129 | return absPath, nil 130 | } 131 | -------------------------------------------------------------------------------- /e2eTests/e2eTerraformAuthPermissionMismatch_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package e2etests 24 | 25 | import ( 26 | "os" 27 | "path" 28 | "runtime" 29 | "testing" 30 | 31 | "github.com/aliveticket/mpf/pkg/infrastructure/authorizationCheckers/terraform" 32 | rgm "github.com/aliveticket/mpf/pkg/infrastructure/resourceGroupManager" 33 | spram "github.com/aliveticket/mpf/pkg/infrastructure/spRoleAssignmentManager" 34 | "github.com/aliveticket/mpf/pkg/usecase" 35 | log "github.com/sirupsen/logrus" 36 | "github.com/stretchr/testify/assert" 37 | ) 38 | 39 | //authorizationFailedErrMsg 40 | 41 | func TestTerraformAuthorizationPermissionMismatch(t *testing.T) { 42 | 43 | // import errors can occur for some resources, when identity does not have all required permissions, 44 | // as described in https://github.com/hashicorp/terraform-provider-azurerm/issues/27961#issuecomment-2467392936 45 | mpfArgs, err := getTestingMPFArgs() 46 | if err != nil { 47 | t.Skip("required environment variables not set, skipping end to end test") 48 | } 49 | mpfArgs.MPFMode = "terraform" 50 | 51 | var tfpath string 52 | if os.Getenv("MPF_TFPATH") == "" { 53 | t.Skip("Terraform Path TF_PATH not set, skipping end to end test") 54 | } 55 | tfpath = os.Getenv("MPF_TFPATH") 56 | 57 | _, filename, _, _ := runtime.Caller(0) 58 | curDir := path.Dir(filename) 59 | log.Infof("curDir: %s", curDir) 60 | wrkDir := path.Join(curDir, "../samples/terraform/authorization-permission-mismatch") 61 | log.Infof("wrkDir: %s", wrkDir) 62 | ctx := t.Context() 63 | 64 | mpfConfig := getMPFConfig(mpfArgs) 65 | 66 | var rgManager usecase.ResourceGroupManager 67 | var spRoleAssignmentManager usecase.ServicePrincipalRolemAssignmentManager 68 | rgManager = rgm.NewResourceGroupManager(mpfArgs.SubscriptionID) 69 | spRoleAssignmentManager = spram.NewSPRoleAssignmentManager(mpfArgs.SubscriptionID) 70 | 71 | var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner 72 | var mpfService *usecase.MPFService 73 | 74 | initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 75 | permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 76 | deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "") 77 | mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) 78 | 79 | mpfResult, err := mpfService.GetMinimumPermissionsRequired() 80 | if err != nil { 81 | t.Error(err) 82 | } 83 | 84 | assert.NotEmpty(t, mpfResult.RequiredPermissions) 85 | assert.Equal(t, 11, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) 86 | } 87 | -------------------------------------------------------------------------------- /e2eTests/e2eTerraformInvalid_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package e2etests 24 | 25 | import ( 26 | "os" 27 | "path" 28 | "runtime" 29 | "testing" 30 | 31 | "github.com/aliveticket/mpf/pkg/infrastructure/authorizationCheckers/terraform" 32 | rgm "github.com/aliveticket/mpf/pkg/infrastructure/resourceGroupManager" 33 | spram "github.com/aliveticket/mpf/pkg/infrastructure/spRoleAssignmentManager" 34 | "github.com/aliveticket/mpf/pkg/usecase" 35 | log "github.com/sirupsen/logrus" 36 | "github.com/stretchr/testify/assert" 37 | ) 38 | 39 | func TestTerraformACIInvalidVarFile(t *testing.T) { 40 | 41 | mpfArgs, err := getTestingMPFArgs() 42 | if err != nil { 43 | t.Skip("required environment variables not set, skipping end to end test") 44 | } 45 | mpfArgs.MPFMode = "terraform" 46 | 47 | var tfpath string 48 | if os.Getenv("MPF_TFPATH") == "" { 49 | t.Skip("Terraform Path TF_PATH not set, skipping end to end test") 50 | } 51 | tfpath = os.Getenv("MPF_TFPATH") 52 | 53 | _, filename, _, _ := runtime.Caller(0) 54 | curDir := path.Dir(filename) 55 | log.Infof("curDir: %s", curDir) 56 | wrkDir := path.Join(curDir, "../samples/terraform/rg-invalid-tfvars") 57 | log.Infof("wrkDir: %s", wrkDir) 58 | varsFile := path.Join(curDir, "../samples/terraform/rg-invalid-tfvars/dev.vars.tfvars") 59 | log.Infof("varsFile: %s", varsFile) 60 | 61 | ctx := t.Context() 62 | 63 | mpfConfig := getMPFConfig(mpfArgs) 64 | 65 | // azAPIClient := azureAPI.NewAzureAPIClients(mpfArgs.SubscriptionID) 66 | var rgManager usecase.ResourceGroupManager 67 | var spRoleAssignmentManager usecase.ServicePrincipalRolemAssignmentManager 68 | rgManager = rgm.NewResourceGroupManager(mpfArgs.SubscriptionID) 69 | spRoleAssignmentManager = spram.NewSPRoleAssignmentManager(mpfArgs.SubscriptionID) 70 | 71 | var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner 72 | var mpfService *usecase.MPFService 73 | 74 | initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 75 | permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 76 | deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, varsFile, true, "") 77 | mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) 78 | 79 | _, err = mpfService.GetMinimumPermissionsRequired() 80 | assert.Error(t, err) 81 | } 82 | 83 | func TestTerraformACIInvalidTfFile(t *testing.T) { 84 | 85 | mpfArgs, err := getTestingMPFArgs() 86 | if err != nil { 87 | t.Skip("required environment variables not set, skipping end to end test") 88 | } 89 | mpfArgs.MPFMode = "terraform" 90 | 91 | var tfpath string 92 | if os.Getenv("MPF_TFPATH") == "" { 93 | t.Skip("Terraform Path TF_PATH not set, skipping end to end test") 94 | } 95 | tfpath = os.Getenv("MPF_TFPATH") 96 | 97 | _, filename, _, _ := runtime.Caller(0) 98 | curDir := path.Dir(filename) 99 | log.Infof("curDir: %s", curDir) 100 | wrkDir := path.Join(curDir, "../samples/terraform/rg-invalid-tf-file") 101 | log.Infof("wrkDir: %s", wrkDir) 102 | 103 | ctx := t.Context() 104 | 105 | mpfConfig := getMPFConfig(mpfArgs) 106 | 107 | // azAPIClient := azureAPI.NewAzureAPIClients(mpfArgs.SubscriptionID) 108 | var rgManager usecase.ResourceGroupManager 109 | var spRoleAssignmentManager usecase.ServicePrincipalRolemAssignmentManager 110 | rgManager = rgm.NewResourceGroupManager(mpfArgs.SubscriptionID) 111 | spRoleAssignmentManager = spram.NewSPRoleAssignmentManager(mpfArgs.SubscriptionID) 112 | 113 | var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner 114 | var mpfService *usecase.MPFService 115 | 116 | initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 117 | permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 118 | deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "") 119 | mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) 120 | 121 | _, err = mpfService.GetMinimumPermissionsRequired() 122 | assert.Error(t, err) 123 | } 124 | 125 | func TestTerraformACIInvalidTfExec(t *testing.T) { 126 | 127 | mpfArgs, err := getTestingMPFArgs() 128 | if err != nil { 129 | t.Skip("required environment variables not set, skipping end to end test") 130 | } 131 | mpfArgs.MPFMode = "terraform" 132 | 133 | tfpath := "/invalid/path/to/terraform" 134 | 135 | _, filename, _, _ := runtime.Caller(0) 136 | curDir := path.Dir(filename) 137 | log.Infof("curDir: %s", curDir) 138 | wrkDir := path.Join(curDir, "../samples/terraform/rg-no-tfvars") 139 | log.Infof("wrkDir: %s", wrkDir) 140 | 141 | ctx := t.Context() 142 | 143 | mpfConfig := getMPFConfig(mpfArgs) 144 | 145 | // azAPIClient := azureAPI.NewAzureAPIClients(mpfArgs.SubscriptionID) 146 | var rgManager usecase.ResourceGroupManager 147 | var spRoleAssignmentManager usecase.ServicePrincipalRolemAssignmentManager 148 | rgManager = rgm.NewResourceGroupManager(mpfArgs.SubscriptionID) 149 | spRoleAssignmentManager = spram.NewSPRoleAssignmentManager(mpfArgs.SubscriptionID) 150 | 151 | var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner 152 | var mpfService *usecase.MPFService 153 | 154 | initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 155 | permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 156 | deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "") 157 | mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) 158 | 159 | _, err = mpfService.GetMinimumPermissionsRequired() 160 | assert.Error(t, err) 161 | } 162 | -------------------------------------------------------------------------------- /e2eTests/e2eTerraformWithImportAndTargeting_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package e2etests 24 | 25 | import ( 26 | "os" 27 | "path" 28 | "runtime" 29 | "testing" 30 | 31 | "github.com/aliveticket/mpf/pkg/infrastructure/authorizationCheckers/terraform" 32 | rgm "github.com/aliveticket/mpf/pkg/infrastructure/resourceGroupManager" 33 | spram "github.com/aliveticket/mpf/pkg/infrastructure/spRoleAssignmentManager" 34 | "github.com/aliveticket/mpf/pkg/usecase" 35 | log "github.com/sirupsen/logrus" 36 | "github.com/stretchr/testify/assert" 37 | ) 38 | 39 | func TestTerraformWithImport(t *testing.T) { 40 | 41 | // import errors can occur for some resources, when identity does not have all required permissions, 42 | // as described in https://github.com/hashicorp/terraform-provider-azurerm/issues/27961#issuecomment-2467392936 43 | mpfArgs, err := getTestingMPFArgs() 44 | if err != nil { 45 | t.Skip("required environment variables not set, skipping end to end test") 46 | } 47 | mpfArgs.MPFMode = "terraform" 48 | 49 | var tfpath string 50 | if os.Getenv("MPF_TFPATH") == "" { 51 | t.Skip("Terraform Path TF_PATH not set, skipping end to end test") 52 | } 53 | tfpath = os.Getenv("MPF_TFPATH") 54 | 55 | _, filename, _, _ := runtime.Caller(0) 56 | curDir := path.Dir(filename) 57 | log.Infof("curDir: %s", curDir) 58 | wrkDir := path.Join(curDir, "../samples/terraform/existing-resource-import") 59 | log.Infof("wrkDir: %s", wrkDir) 60 | ctx := t.Context() 61 | 62 | mpfConfig := getMPFConfig(mpfArgs) 63 | 64 | var rgManager usecase.ResourceGroupManager 65 | var spRoleAssignmentManager usecase.ServicePrincipalRolemAssignmentManager 66 | rgManager = rgm.NewResourceGroupManager(mpfArgs.SubscriptionID) 67 | spRoleAssignmentManager = spram.NewSPRoleAssignmentManager(mpfArgs.SubscriptionID) 68 | 69 | var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner 70 | var mpfService *usecase.MPFService 71 | 72 | initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 73 | permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 74 | deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "") 75 | mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) 76 | 77 | mpfResult, err := mpfService.GetMinimumPermissionsRequired() 78 | if err != nil { 79 | t.Error(err) 80 | } 81 | 82 | assert.NotEmpty(t, mpfResult.RequiredPermissions) 83 | assert.Equal(t, 17, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) 84 | } 85 | 86 | func TestTerraformWithTargetting(t *testing.T) { 87 | 88 | // import errors can occur for some resources, when identity does not have all required permissions, 89 | // as described in https://github.com/hashicorp/terraform-provider-azurerm/issues/27961#issuecomment-2467392936 90 | mpfArgs, err := getTestingMPFArgs() 91 | if err != nil { 92 | t.Skip("required environment variables not set, skipping end to end test") 93 | } 94 | mpfArgs.MPFMode = "terraform" 95 | 96 | var tfpath string 97 | if os.Getenv("MPF_TFPATH") == "" { 98 | t.Skip("Terraform Path TF_PATH not set, skipping end to end test") 99 | } 100 | tfpath = os.Getenv("MPF_TFPATH") 101 | 102 | _, filename, _, _ := runtime.Caller(0) 103 | curDir := path.Dir(filename) 104 | log.Infof("curDir: %s", curDir) 105 | wrkDir := path.Join(curDir, "../samples/terraform/module-test-with-targetting") 106 | log.Infof("wrkDir: %s", wrkDir) 107 | ctx := t.Context() 108 | 109 | mpfConfig := getMPFConfig(mpfArgs) 110 | 111 | var rgManager usecase.ResourceGroupManager 112 | var spRoleAssignmentManager usecase.ServicePrincipalRolemAssignmentManager 113 | rgManager = rgm.NewResourceGroupManager(mpfArgs.SubscriptionID) 114 | spRoleAssignmentManager = spram.NewSPRoleAssignmentManager(mpfArgs.SubscriptionID) 115 | 116 | var deploymentAuthorizationCheckerCleaner usecase.DeploymentAuthorizationCheckerCleaner 117 | var mpfService *usecase.MPFService 118 | 119 | initialPermissionsToAdd := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 120 | permissionsToAddToResult := []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"} 121 | deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(wrkDir, tfpath, "", true, "module.law") 122 | mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false) 123 | 124 | mpfResult, err := mpfService.GetMinimumPermissionsRequired() 125 | if err != nil { 126 | t.Error(err) 127 | } 128 | 129 | assert.NotEmpty(t, mpfResult.RequiredPermissions) 130 | assert.Equal(t, 8, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) 131 | } 132 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aliveticket/mpf 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/Azure/azure-sdk-for-go v68.0.0+incompatible 7 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 8 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 9 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 10 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 11 | github.com/Azure/go-autorest/autorest v0.11.30 12 | github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 13 | github.com/google/uuid v1.6.0 14 | github.com/hashicorp/terraform-exec v0.23.0 15 | github.com/sirupsen/logrus v1.9.3 16 | github.com/spf13/cobra v1.9.1 17 | github.com/spf13/pflag v1.0.6 18 | github.com/spf13/viper v1.20.1 19 | github.com/stretchr/testify v1.10.0 20 | ) 21 | 22 | require ( 23 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect 24 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 25 | github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect 26 | github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 // indirect 27 | github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect 28 | github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect 29 | github.com/Azure/go-autorest/autorest/validation v0.3.2 // indirect 30 | github.com/Azure/go-autorest/logger v0.2.2 // indirect 31 | github.com/Azure/go-autorest/tracing v0.6.1 // indirect 32 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect 33 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 34 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 35 | github.com/dimchansky/utfbom v1.1.1 // indirect 36 | github.com/fsnotify/fsnotify v1.8.0 // indirect 37 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 38 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 39 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 40 | github.com/hashicorp/go-version v1.7.0 // indirect 41 | github.com/hashicorp/terraform-json v0.24.0 // indirect 42 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 43 | github.com/kylelemons/godebug v1.1.0 // indirect 44 | github.com/mitchellh/go-homedir v1.1.0 // indirect 45 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 46 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 47 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 48 | github.com/sagikazarmark/locafero v0.7.0 // indirect 49 | github.com/sourcegraph/conc v0.3.0 // indirect 50 | github.com/spf13/afero v1.12.0 // indirect 51 | github.com/spf13/cast v1.7.1 // indirect 52 | github.com/subosito/gotenv v1.6.0 // indirect 53 | github.com/zclconf/go-cty v1.16.2 // indirect 54 | go.uber.org/multierr v1.11.0 // indirect 55 | golang.org/x/crypto v0.36.0 // indirect 56 | golang.org/x/net v0.38.0 // indirect 57 | golang.org/x/sys v0.31.0 // indirect 58 | golang.org/x/text v0.23.0 // indirect 59 | gopkg.in/yaml.v3 v3.0.1 // indirect 60 | ) 61 | -------------------------------------------------------------------------------- /pkg/domain/InvalidActionOrNotActionErrorParser.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func GetDeleteActionFromInvalidActionOrNotActionError(invalidActionErrMesg string) ([]string, error) { 12 | 13 | // parse error message to get the delete action 14 | // {"error":{"code":"InvalidActionOrNotAction","message":"'Microsoft.Insights/components/currentbillingfeatures/delete' does not match any of the actions supported by the providers."}} 15 | 16 | invalidActions := []string{} 17 | if invalidActionErrMesg != "" && !strings.Contains(invalidActionErrMesg, "InvalidActionOrNotAction") { 18 | log.Infoln("Non InvalidActionOrNotAction Error when creating deployment:", invalidActionErrMesg) 19 | return invalidActions, errors.New("Could not parse deploment error, potentially due to a Non-InvalidActionOrNotAction error") 20 | } 21 | 22 | re := regexp.MustCompile(`"message":"\'([^']+delete)\' does not match any of the actions supported by the providers."`) 23 | matches := re.FindAllStringSubmatch(invalidActionErrMesg, -1) 24 | 25 | if len(matches) == 0 { 26 | return invalidActions, errors.New("No matches found in 'invalidActionErrorMessage' error message") 27 | } 28 | 29 | for _, match := range matches { 30 | if len(match) == 2 { 31 | invalidActions = append(invalidActions, match[1]) 32 | } 33 | } 34 | 35 | return invalidActions, nil 36 | 37 | } 38 | -------------------------------------------------------------------------------- /pkg/domain/InvalidActionOrNotActionErrorParser_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package domain 24 | 25 | import ( 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func TestGetDeleteActionFromInvalidActionOrNotActionError(t *testing.T) { 32 | tests := []struct { 33 | name string 34 | input string 35 | expectedActions []string 36 | expectedError bool 37 | expectedErrorString string 38 | }{ 39 | { 40 | name: "Valid delete action", 41 | input: `{"error":{"code":"InvalidActionOrNotAction","message":"'Microsoft.Insights/components/currentbillingfeatures/delete' does not match any of the actions supported by the providers."}}`, 42 | expectedActions: []string{"Microsoft.Insights/components/currentbillingfeatures/delete"}, 43 | expectedError: false, 44 | }, 45 | { 46 | name: "Non InvalidActionOrNotAction error", 47 | input: `{"error":{"code":"SomeOtherError","message":"'Microsoft.Insights/components/currentbillingfeatures/delete' does not match any of the actions supported by the providers."}}`, 48 | expectedActions: []string{}, 49 | expectedError: true, 50 | expectedErrorString: "Could not parse deploment error, potentially due to a Non-InvalidActionOrNotAction error", 51 | }, 52 | { 53 | name: "No matches found", 54 | input: `{"error":{"code":"InvalidActionOrNotAction","message":"No delete action here"}}`, 55 | expectedActions: []string{}, 56 | expectedError: true, 57 | expectedErrorString: "No matches found in 'invalidActionErrorMessage' error message", 58 | }, 59 | { 60 | name: "Empty input", 61 | input: "", 62 | expectedActions: []string{}, 63 | expectedError: true, 64 | expectedErrorString: "No matches found in 'invalidActionErrorMessage' error message", 65 | }, 66 | } 67 | 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | actions, err := GetDeleteActionFromInvalidActionOrNotActionError(tt.input) 71 | assert.Equal(t, tt.expectedActions, actions) 72 | if tt.expectedError { 73 | assert.Error(t, err) 74 | assert.EqualError(t, err, tt.expectedErrorString) 75 | } else { 76 | assert.NoError(t, err) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/domain/appendPermissionsForSpecialCases.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | ) 6 | 7 | var toAppendSpecialCasePermissions = map[string][]string{ 8 | 9 | // Below permissions are required for Microsoft.Insights/components due to the following issue: 10 | // https://github.com/hashicorp/terraform-provider-azurerm/issues/27961#issuecomment-2467392936 11 | 12 | "Microsoft.Insights/components/read": { 13 | "Microsoft.Insights/components/currentbillingfeatures/read", 14 | "Microsoft.AlertsManagement/smartDetectorAlertRules/read", 15 | }, 16 | "Microsoft.Insights/components/write": { 17 | "Microsoft.Insights/components/currentbillingfeatures/write", 18 | "Microsoft.AlertsManagement/smartDetectorAlertRules/write", 19 | }, 20 | } 21 | 22 | func appendPermissionsForSpecialCases(scpPerms map[string][]string) map[string][]string { 23 | for scp, perms := range scpPerms { 24 | for _, perm := range perms { 25 | if toAppend, ok := toAppendSpecialCasePermissions[perm]; ok { 26 | scpPerms[scp] = append(scpPerms[scp], toAppend...) 27 | log.Infof("Appended special case permissions for scope %s: %v", scp, toAppend) 28 | } 29 | } 30 | } 31 | return scpPerms 32 | } 33 | -------------------------------------------------------------------------------- /pkg/domain/appendPermissionsForSpecialCases_test.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestAppendPermissiosnForSpecialCases(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | input map[string][]string 12 | expected map[string][]string 13 | }{ 14 | { 15 | name: "No additional permissions appended", 16 | input: map[string][]string{ 17 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44": {"Microsoft.ContainerService/managedClusters/read", "Microsoft.ContainerService/managedClusters/write"}, 18 | }, 19 | expected: map[string][]string{ 20 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44": {"Microsoft.ContainerService/managedClusters/read", "Microsoft.ContainerService/managedClusters/write"}, 21 | }, 22 | }, 23 | { 24 | name: "No additional permissions appended for multiple scopes", 25 | input: map[string][]string{ 26 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X47": {"Microsoft.ContainerService/managedClusters/read", "Microsoft.ContainerService/managedClusters/write"}, 27 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X48": {"Microsoft.ContainerService/managedClusters/read", "Microsoft.ContainerService/managedClusters/write"}, 28 | }, 29 | expected: map[string][]string{ 30 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X47": {"Microsoft.ContainerService/managedClusters/read", "Microsoft.ContainerService/managedClusters/write"}, 31 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X48": {"Microsoft.ContainerService/managedClusters/read", "Microsoft.ContainerService/managedClusters/write"}, 32 | }, 33 | }, 34 | { 35 | name: "Append for multiple scopes and permissions", 36 | input: map[string][]string{ 37 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X47": {"Microsoft.ContainerService/managedClusters/read", "Microsoft.ContainerService/managedClusters/write", "Microsoft.Insights/components/read"}, 38 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X48": {"Microsoft.ContainerService/managedClusters/read", "Microsoft.ContainerService/managedClusters/write", "Microsoft.Insights/components/write"}, 39 | }, 40 | expected: map[string][]string{ 41 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X47": { 42 | "Microsoft.ContainerService/managedClusters/read", 43 | "Microsoft.ContainerService/managedClusters/write", 44 | "Microsoft.Insights/components/read", 45 | "Microsoft.Insights/components/currentbillingfeatures/read", 46 | "Microsoft.AlertsManagement/smartDetectorAlertRules/read", 47 | }, 48 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X48": { 49 | "Microsoft.ContainerService/managedClusters/read", 50 | "Microsoft.ContainerService/managedClusters/write", 51 | "Microsoft.Insights/components/write", 52 | "Microsoft.Insights/components/currentbillingfeatures/write", 53 | "Microsoft.AlertsManagement/smartDetectorAlertRules/write", 54 | }, 55 | }, 56 | }, 57 | { 58 | name: "Additional permissions appended", 59 | input: map[string][]string{ 60 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44": {"Microsoft.Insights/components/read"}, 61 | }, 62 | expected: map[string][]string{ 63 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44": { 64 | "Microsoft.Insights/components/read", 65 | "Microsoft.Insights/components/currentbillingfeatures/read", 66 | "Microsoft.AlertsManagement/smartDetectorAlertRules/read", 67 | }, 68 | }, 69 | }, 70 | { 71 | name: "Additional permissions appended for write", 72 | input: map[string][]string{ 73 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44": {"Microsoft.Insights/components/write"}, 74 | }, 75 | expected: map[string][]string{ 76 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44": { 77 | "Microsoft.Insights/components/write", 78 | "Microsoft.Insights/components/currentbillingfeatures/write", 79 | "Microsoft.AlertsManagement/smartDetectorAlertRules/write", 80 | }, 81 | }, 82 | }, 83 | { 84 | name: "Additional permissions appended for multiple scopes", 85 | input: map[string][]string{ 86 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X47": {"Microsoft.Insights/components/read"}, 87 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X48": {"Microsoft.Insights/components/write"}, 88 | }, 89 | expected: map[string][]string{ 90 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X47": { 91 | "Microsoft.Insights/components/read", 92 | "Microsoft.Insights/components/currentbillingfeatures/read", 93 | "Microsoft.AlertsManagement/smartDetectorAlertRules/read", 94 | }, 95 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X48": { 96 | "Microsoft.Insights/components/write", 97 | "Microsoft.Insights/components/currentbillingfeatures/write", 98 | "Microsoft.AlertsManagement/smartDetectorAlertRules/write", 99 | }, 100 | }, 101 | }, 102 | { 103 | name: "Empty input", 104 | input: map[string][]string{}, 105 | expected: map[string][]string{}, 106 | }, 107 | } 108 | 109 | for _, tt := range tests { 110 | t.Run(tt.name, func(t *testing.T) { 111 | result := appendPermissionsForSpecialCases(tt.input) 112 | if !reflect.DeepEqual(result, tt.expected) { 113 | t.Errorf("expected %v, got %v", tt.expected, result) 114 | } 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /pkg/domain/authorizationErrorParser.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package domain 24 | 25 | import ( 26 | "errors" 27 | "fmt" 28 | "regexp" 29 | "strings" 30 | 31 | log "github.com/sirupsen/logrus" 32 | ) 33 | 34 | func GetScopePermissionsFromAuthError(authErrMesg string) (map[string][]string, error) { 35 | log.Debugf("Attempting to Parse Authorization Error: %s", authErrMesg) 36 | if authErrMesg != "" && !strings.Contains(authErrMesg, "AuthorizationFailed") && !strings.Contains(authErrMesg, "Authorization failed") && !strings.Contains(authErrMesg, "AuthorizationPermissionMismatch") && !strings.Contains(authErrMesg, "LinkedAccessCheckFailed") { 37 | log.Warnln("Non Authorization Error when creating deployment:", authErrMesg) 38 | return nil, errors.New("Could not parse deploment error, potentially due to a Non-Authorization error") 39 | } 40 | 41 | var resMap map[string][]string 42 | var err error 43 | 44 | switch { 45 | case strings.Count(authErrMesg, "LinkedAuthorizationFailed") >= 1: 46 | log.Debug("Parsing LinkedAuthorizationFailed Error") 47 | resMap, err = parseLinkedAuthorizationFailedErrors(authErrMesg) 48 | case strings.Count(authErrMesg, "AuthorizationFailed") >= 1: 49 | log.Debug("Parsing AuthorizationFailed Error") 50 | resMap, err = parseMultiAuthorizationFailedErrors(authErrMesg) 51 | case strings.Count(authErrMesg, "Authorization failed") >= 1: 52 | log.Debug("Parsing Authorization failed Error") 53 | resMap, err = parseMultiAuthorizationErrors(authErrMesg) 54 | case strings.Count(authErrMesg, "AuthorizationPermissionMismatch") >= 1: 55 | log.Debug("Parsing AuthorizationPermissionMismatch Error") 56 | resMap, err = parseAuthorizationPermissionMismatchError(authErrMesg) 57 | case strings.Count(authErrMesg, "LinkedAccessCheckFailed") >= 1: 58 | log.Debug("Parsing LinkedAccessCheckFailed Error") 59 | resMap, err = parseLinkedAccessCheckFailedError(authErrMesg) 60 | } 61 | 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | // If map is empty, return error 67 | if len(resMap) == 0 { 68 | return nil, fmt.Errorf("Could not parse deployment error for scope/permissions: %s", authErrMesg) 69 | } 70 | 71 | return appendPermissionsForSpecialCases(resMap), nil 72 | } 73 | 74 | // For 'AuthorizationFailed' errors 75 | func parseMultiAuthorizationFailedErrors(authorizationFailedErrMsg string) (map[string][]string, error) { 76 | 77 | re := regexp.MustCompile(`The client '([^']+)' with object id '([^']+)' does not have authorization to perform action '([^']+)'.* over scope '([^']+)' or the scope is invalid\.`) 78 | 79 | matches := re.FindAllStringSubmatch(authorizationFailedErrMsg, -1) 80 | 81 | if len(matches) == 0 { 82 | return nil, errors.New("No matches found in 'AuthorizationFailed' error message") 83 | } 84 | 85 | scopePermissionsMap := make(map[string][]string) 86 | 87 | // Iterate through the matches and populate the map 88 | for _, match := range matches { 89 | if len(match) == 5 { 90 | // resourceType := match[1] 91 | action := match[3] 92 | scope := match[4] 93 | 94 | if _, ok := scopePermissionsMap[scope]; !ok { 95 | scopePermissionsMap[scope] = make([]string, 0) 96 | } 97 | scopePermissionsMap[scope] = append(scopePermissionsMap[scope], action) 98 | } 99 | } 100 | 101 | // if map is empty, return error 102 | if len(scopePermissionsMap) == 0 { 103 | return nil, errors.New("No scope/permissions found in Multi error message") 104 | } 105 | 106 | return scopePermissionsMap, nil 107 | 108 | } 109 | 110 | // For 'Authorization failed' errors 111 | func parseMultiAuthorizationErrors(authorizationFailedErrMsg string) (map[string][]string, error) { 112 | 113 | // Regular expression to extract resource information 114 | re := regexp.MustCompile(`Authorization failed for template resource '([^']+)' of type '([^']+)'\. The client '([^']+)' with object id '([^']+)' does not have permission to perform action '([^']+)' at scope '([^']+)'\.`) 115 | 116 | // Find all matches in the error message 117 | matches := re.FindAllStringSubmatch(authorizationFailedErrMsg, -1) 118 | 119 | // If No Matches found return error 120 | if len(matches) == 0 { 121 | return nil, errors.New("No matches found in 'Authorization failed' error message") 122 | } 123 | 124 | // Create a map to store scope/permissions 125 | scopePermissionsMap := make(map[string][]string) 126 | 127 | // Iterate through the matches and populate the map 128 | for _, match := range matches { 129 | if len(match) == 7 { 130 | // resourceType := match[1] 131 | action := match[5] 132 | scope := match[6] 133 | 134 | if _, ok := scopePermissionsMap[scope]; !ok { 135 | scopePermissionsMap[scope] = make([]string, 0) 136 | } 137 | scopePermissionsMap[scope] = append(scopePermissionsMap[scope], action) 138 | } 139 | } 140 | 141 | // if map is empty, return error 142 | if len(scopePermissionsMap) == 0 { 143 | return nil, errors.New("No scope/permissions found in Multi error message") 144 | } 145 | 146 | return scopePermissionsMap, nil 147 | 148 | } 149 | 150 | // For 'LinkedAuthorizationFailed' errors 151 | func parseLinkedAuthorizationFailedErrors(authorizationFailedErrMsg string) (map[string][]string, error) { 152 | 153 | // Regular expression to extract resource information 154 | // re := regexp.MustCompile(`Authorization failed for template resource '([^']+)' of type '([^']+)'\. The client '([^']+)' with object id '([^']+)' does not have permission to perform action '([^']+)' at scope '([^']+)'\.`) 155 | 156 | // Find regular expressions to pull action and scope from error message "does not have permission to perform action(s) 'Microsoft.Network/virtualNetworks/subnets/join/action' on the linked scope(s) '/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/az-mpf-tf-test-rg/providers/Microsoft.Network/virtualNetworks/vnet-32a70ccbb3247e2b/subnets/subnet-32a70ccbb3247e2b' (respectively) or the linked scope(s) are invalid". 157 | re := regexp.MustCompile(`does not have permission to perform action\(s\) '([^']+)' on the linked scope\(s\) '([^']+)' \(respectively\) or the linked scope\(s\) are invalid`) 158 | 159 | // Find all matches in the error message 160 | matches := re.FindAllStringSubmatch(authorizationFailedErrMsg, -1) 161 | 162 | // If No Matches found return error 163 | if len(matches) == 0 { 164 | return nil, errors.New("No matches found in 'Authorization failed' error message") 165 | } 166 | 167 | // Create a map to store scope/permissions 168 | scopePermissionsMap := make(map[string][]string) 169 | 170 | // Iterate through the matches and populate the map 171 | for _, match := range matches { 172 | if len(match) == 3 { 173 | // resourceType := match[1] 174 | action := match[1] 175 | scope := match[2] 176 | 177 | if _, ok := scopePermissionsMap[scope]; !ok { 178 | scopePermissionsMap[scope] = make([]string, 0) 179 | } 180 | scopePermissionsMap[scope] = append(scopePermissionsMap[scope], action) 181 | } 182 | } 183 | 184 | // if map is empty, return error 185 | if len(scopePermissionsMap) == 0 { 186 | return nil, errors.New("No scope/permissions found in Multi error message") 187 | } 188 | 189 | return scopePermissionsMap, nil 190 | 191 | } 192 | -------------------------------------------------------------------------------- /pkg/domain/authorizationPermissionMismatchErrorParser.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package domain 24 | 25 | import ( 26 | "errors" 27 | "fmt" 28 | "log" 29 | "regexp" 30 | ) 31 | 32 | type PermissionErrorDetails struct { 33 | MissingPermissions string 34 | Scope string 35 | } 36 | 37 | func parseAuthorizationPermissionMismatchError(authorizationFailedErrMsg string) (map[string][]string, error) { 38 | 39 | log.Printf("Attempting to Parse AuthorizationPermissionMismatch Error: @@%s@@", authorizationFailedErrMsg) 40 | re := regexp.MustCompile(`retrieving (queue|file|blob) properties for Storage Account \(Subscription: \"([^"]+)\"\nResource Group Name: \"([^"]+)\"\nStorage Account Name: \"([^"]+)\"\): executing request: unexpected status 403 \(403 This request is not authorized to perform this operation using this permission.\) with AuthorizationPermissionMismatch: This request is not authorized to perform this operation using this permission.`) 41 | 42 | matches := re.FindAllStringSubmatch(authorizationFailedErrMsg, -1) 43 | 44 | if len(matches) == 0 { 45 | return nil, errors.New("no matches found in 'AuthorizationPermissionMismatch' error message") 46 | } 47 | 48 | scopePermissionsMap := make(map[string][]string) 49 | 50 | // Iterate through the matches and populate the map 51 | for _, match := range matches { 52 | if len(match) == 5 { 53 | var action string 54 | switch match[1] { 55 | case "queue": 56 | action = "Microsoft.Storage/storageAccounts/queueServices/read" 57 | case "file": 58 | action = "Microsoft.Storage/storageAccounts/fileServices/read" 59 | case "blob": 60 | action = "Microsoft.Storage/storageAccounts/blobServices/read" 61 | } 62 | 63 | scope := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Storage/storageAccounts/%s", match[2], match[3], match[4]) 64 | 65 | if _, ok := scopePermissionsMap[scope]; !ok { 66 | scopePermissionsMap[scope] = make([]string, 0) 67 | } 68 | scopePermissionsMap[scope] = append(scopePermissionsMap[scope], action) 69 | } 70 | } 71 | 72 | // if map is empty, return error 73 | if len(scopePermissionsMap) == 0 { 74 | return nil, errors.New("No scope/permissions found in AuthorizationPermissionMismatch error message") 75 | } 76 | 77 | return scopePermissionsMap, nil 78 | 79 | } 80 | -------------------------------------------------------------------------------- /pkg/domain/authorizationPermissionMismatchErrorParser_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package domain 24 | 25 | import ( 26 | "reflect" 27 | "testing" 28 | ) 29 | 30 | func TestParseAuthorizationPermissionMismatchError(t *testing.T) { 31 | tests := []struct { 32 | name string 33 | input string 34 | want map[string][]string 35 | wantErr bool 36 | }{ 37 | { 38 | name: "Valid queue error message", 39 | input: `retrieving queue properties for Storage Account (Subscription: "SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS" 40 | Resource Group Name: "rg-nygst" 41 | Storage Account Name: "sanygst"): executing request: unexpected status 403 (403 This request is not authorized to perform this operation using this permission.) with AuthorizationPermissionMismatch: This request is not authorized to perform this operation using this permission.`, 42 | want: map[string][]string{ 43 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/rg-nygst/providers/Microsoft.Storage/storageAccounts/sanygst": {"Microsoft.Storage/storageAccounts/queueServices/read"}, 44 | }, 45 | wantErr: false, 46 | }, 47 | { 48 | name: "Valid file error message", 49 | input: `retrieving file properties for Storage Account (Subscription: "SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS" 50 | Resource Group Name: "rg-nygst" 51 | Storage Account Name: "sanygst"): executing request: unexpected status 403 (403 This request is not authorized to perform this operation using this permission.) with AuthorizationPermissionMismatch: This request is not authorized to perform this operation using this permission.`, 52 | want: map[string][]string{ 53 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/rg-nygst/providers/Microsoft.Storage/storageAccounts/sanygst": {"Microsoft.Storage/storageAccounts/fileServices/read"}, 54 | }, 55 | wantErr: false, 56 | }, 57 | { 58 | name: "Valid blob error message", 59 | input: `retrieving blob properties for Storage Account (Subscription: "SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS" 60 | Resource Group Name: "rg-nygst" 61 | Storage Account Name: "sanygst"): executing request: unexpected status 403 (403 This request is not authorized to perform this operation using this permission.) with AuthorizationPermissionMismatch: This request is not authorized to perform this operation using this permission.`, 62 | want: map[string][]string{ 63 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/rg-nygst/providers/Microsoft.Storage/storageAccounts/sanygst": {"Microsoft.Storage/storageAccounts/blobServices/read"}, 64 | }, 65 | wantErr: false, 66 | }, 67 | { 68 | name: "Invalid error message", 69 | input: `some invalid error message`, 70 | want: nil, 71 | wantErr: true, 72 | }, 73 | { 74 | name: "Empty error message", 75 | input: ``, 76 | want: nil, 77 | wantErr: true, 78 | }, 79 | { 80 | name: "Invalid Format Error Message", 81 | input: `retrieving blob properties for Storage Account (Subscription: "SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS" 82 | Resource Group Name: "rg-nygst" WRONG WRONG WRONG 83 | Storage Account Name: "sanygst"): executing request: unexpected status 403 (403 This request is not authorized to perform this operation using this permission.) with AuthorizationPermissionMismatch: This request is not authorized to perform this operation using this permission.`, 84 | want: nil, 85 | wantErr: true, 86 | }, 87 | } 88 | for _, tt := range tests { 89 | t.Run(tt.name, func(t *testing.T) { 90 | got, err := parseAuthorizationPermissionMismatchError(tt.input) 91 | if (err != nil) != tt.wantErr { 92 | t.Errorf("parseAuthorizationPermissionMismatchError() error = %v, wantErr %v", err, tt.wantErr) 93 | return 94 | } 95 | if !reflect.DeepEqual(got, tt.want) { 96 | t.Errorf("parseAuthorizationPermissionMismatchError() = %v, want %v", got, tt.want) 97 | } 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /pkg/domain/linkedAccessCheckFailedErrorParser.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package domain 24 | 25 | import ( 26 | "errors" 27 | "log" 28 | "regexp" 29 | ) 30 | 31 | func parseLinkedAccessCheckFailedError(authorizationFailedErrMsg string) (map[string][]string, error) { 32 | 33 | log.Printf("Attempting to Parse LinkedAccessCheckFailedError Error: %s", authorizationFailedErrMsg) 34 | 35 | re := regexp.MustCompile(`The client with object id '([^']+)' does not have authorization to perform action '([^']+)'.* over scope '([^']+)' or the scope is invalid\.`) 36 | 37 | matches := re.FindAllStringSubmatch(authorizationFailedErrMsg, -1) 38 | 39 | if len(matches) == 0 { 40 | return nil, errors.New("No matches found in 'LinkedAccessCheckFailedError' error message") 41 | } 42 | 43 | scopePermissionsMap := make(map[string][]string) 44 | 45 | // Iterate through the matches and populate the map 46 | for _, match := range matches { 47 | if len(match) == 4 { 48 | // resourceType := match[1] 49 | action := match[2] 50 | scope := match[3] 51 | 52 | if _, ok := scopePermissionsMap[scope]; !ok { 53 | scopePermissionsMap[scope] = make([]string, 0) 54 | } 55 | scopePermissionsMap[scope] = append(scopePermissionsMap[scope], action) 56 | } 57 | } 58 | 59 | // if map is empty, return error 60 | if len(scopePermissionsMap) == 0 { 61 | return nil, errors.New("No scope/permissions found in LinkedAccessCheckFailedError message") 62 | } 63 | 64 | return scopePermissionsMap, nil 65 | 66 | } 67 | -------------------------------------------------------------------------------- /pkg/domain/linkedAccessCheckFailedErrorParser_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package domain 24 | 25 | import ( 26 | "reflect" 27 | "testing" 28 | ) 29 | 30 | func TestParseLinkedAccessCheckFailedError(t *testing.T) { 31 | tests := []struct { 32 | name string 33 | input string 34 | want map[string][]string 35 | wantErr bool 36 | }{ 37 | { 38 | name: "Valid error message", 39 | input: "Virtual Network Gateway Name: \"vpn-bhlqb\"): performing CreateOrUpdate: unexpected status 400 (400 Bad Request) with error: LinkedAccessCheckFailed: The client with object id 'ddfcf162-2cf2-40cf-bd4a-49a63e248436' does not have authorization to perform action 'Microsoft.Network/publicIPAddresses/join/action' over scope '/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/rg-bhlqb/providers/Microsoft.Network/publicIPAddresses/vpn-pip-bhlqb' or the scope is invalid. For details on the required permissions, please visit 'https://aka.ms/vngwroles'. If access was recently granted, please refresh your credentials.", 40 | want: map[string][]string{ 41 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/rg-bhlqb/providers/Microsoft.Network/publicIPAddresses/vpn-pip-bhlqb": {"Microsoft.Network/publicIPAddresses/join/action"}, 42 | }, 43 | wantErr: false, 44 | }, 45 | { 46 | name: "Invalid error message", 47 | input: "Invalid error message", 48 | want: nil, 49 | wantErr: true, 50 | }, 51 | { 52 | name: "Edge case with empty input", 53 | input: "", 54 | want: nil, 55 | wantErr: true, 56 | }, 57 | } 58 | 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | got, err := parseLinkedAccessCheckFailedError(tt.input) 62 | if (err != nil) != tt.wantErr { 63 | t.Errorf("parseLinkedAccessCheckFailedError() error = %v, wantErr %v", err, tt.wantErr) 64 | return 65 | } 66 | if !reflect.DeepEqual(got, tt.want) { 67 | t.Errorf("parseLinkedAccessCheckFailedError() = %v, want %v", got, tt.want) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pkg/domain/mpfConfig.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package domain 24 | 25 | type Role struct { 26 | RoleDefinitionID string 27 | RoleDefinitionName string 28 | RoleDefinitionDescription string 29 | RoleDefinitionResourceID string 30 | } 31 | 32 | type ResourceGroup struct { 33 | ResourceGroupName string 34 | ResourceGroupResourceID string 35 | Location string 36 | } 37 | 38 | type ServicePrincipal struct { 39 | SPClientID string 40 | SPObjectID string 41 | SPClientSecret string 42 | } 43 | 44 | type MPFConfig struct { 45 | ResourceGroup ResourceGroup 46 | SubscriptionID string 47 | TenantID string 48 | SP ServicePrincipal 49 | Role Role 50 | } 51 | 52 | type MPFResult struct { 53 | // The map from which the minimum permissions will be calculated 54 | RequiredPermissions map[string][]string 55 | } 56 | 57 | func GetMPFResult(requiredPermissions map[string][]string) MPFResult { 58 | return MPFResult{ 59 | RequiredPermissions: getMapWithUniqueValues(requiredPermissions), 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/domain/mpfResultFilterSort.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package domain 24 | 25 | import "sort" 26 | 27 | func getMapWithUniqueValues(m map[string][]string) map[string][]string { 28 | sm := make(map[string][]string) 29 | for key, vals := range m { 30 | vals = getUniqueSlice(vals) 31 | sort.Strings(vals) 32 | sm[key] = vals 33 | } 34 | return sm 35 | } 36 | 37 | func getUniqueSlice(s []string) []string { 38 | uniqueSlice := make([]string, 0, len(s)) 39 | m := make(map[string]bool) 40 | for _, val := range s { 41 | if _, ok := m[val]; !ok { 42 | m[val] = true 43 | uniqueSlice = append(uniqueSlice, val) 44 | } 45 | } 46 | return uniqueSlice 47 | } 48 | -------------------------------------------------------------------------------- /pkg/domain/mpfResultFilterSort_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package domain 24 | 25 | import ( 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func TestGetUniqueSlice(t *testing.T) { 32 | 33 | t.Parallel() 34 | 35 | tests := []struct { 36 | name string 37 | slice []string 38 | expected []string 39 | }{ 40 | { 41 | name: "Empty slice", 42 | slice: []string{}, 43 | expected: []string{}, 44 | }, 45 | { 46 | name: "Slice with duplicate values", 47 | slice: []string{"apple", "banana", "apple", "orange", "banana"}, 48 | expected: []string{"apple", "banana", "orange"}, 49 | }, 50 | { 51 | name: "Slice with unique values", 52 | slice: []string{"apple", "banana", "orange"}, 53 | expected: []string{"apple", "banana", "orange"}, 54 | }, 55 | } 56 | 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | result := getUniqueSlice(tt.slice) 60 | assert.Equal(t, tt.expected, result) 61 | }) 62 | } 63 | } 64 | 65 | func TestGetMapWithUniqueValues(t *testing.T) { 66 | 67 | t.Parallel() 68 | 69 | tests := []struct { 70 | name string 71 | input map[string][]string 72 | expected map[string][]string 73 | }{ 74 | { 75 | name: "Empty map", 76 | input: map[string][]string{}, 77 | expected: map[string][]string{}, 78 | }, 79 | { 80 | name: "Map with empty slices", 81 | input: map[string][]string{ 82 | "key1": {}, 83 | "key2": {}, 84 | }, 85 | expected: map[string][]string{ 86 | "key1": {}, 87 | "key2": {}, 88 | }, 89 | }, 90 | { 91 | name: "Map with duplicate values", 92 | input: map[string][]string{ 93 | "key1": {"apple", "banana", "apple", "orange", "banana"}, 94 | "key2": {"apple", "banana", "apple", "orange", "banana"}, 95 | }, 96 | expected: map[string][]string{ 97 | "key1": {"apple", "banana", "orange"}, 98 | "key2": {"apple", "banana", "orange"}, 99 | }, 100 | }, 101 | { 102 | name: "Map with unique values", 103 | input: map[string][]string{ 104 | "key1": {"apple", "banana", "orange"}, 105 | "key2": {"apple", "banana", "orange"}, 106 | }, 107 | expected: map[string][]string{ 108 | "key1": {"apple", "banana", "orange"}, 109 | "key2": {"apple", "banana", "orange"}, 110 | }, 111 | }, 112 | } 113 | 114 | for _, tt := range tests { 115 | t.Run(tt.name, func(t *testing.T) { 116 | result := getMapWithUniqueValues(tt.input) 117 | assert.Equal(t, tt.expected, result) 118 | }) 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /pkg/infrastructure/ARMTemplateShared/armTemplateShared.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package ARMTemplateShared 24 | 25 | import "errors" 26 | 27 | var ErrInvalidTemplate = errors.New("InvalidTemplate") 28 | 29 | type ArmTemplateAdditionalConfig struct { 30 | TemplateFilePath string 31 | ParametersFilePath string 32 | DeploymentName string 33 | SubscriptionScoped bool 34 | Location string 35 | } 36 | 37 | // Get parameters in standard format that is without the schema, contentVersion and parameters fields 38 | func GetParametersInStandardFormat(parameters map[string]interface{}) map[string]interface{} { 39 | // convert from 40 | // { 41 | // "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 42 | // "contentVersion": "1.0.0.0", 43 | // "parameters": { 44 | // "adminUsername": { 45 | // "value": "GEN-UNIQUE" 46 | // }, 47 | // "adminPasswordOrKey": { 48 | // "value": "GEN-PASSWORD" 49 | // }, 50 | // "dnsLabelPrefix": { 51 | // "value": "GEN-UNIQUE" 52 | // } 53 | // } 54 | // } 55 | 56 | // convert to 57 | // { 58 | // "adminUsername": { 59 | // "value": "GEN-UNIQUE" 60 | // }, 61 | // "adminPasswordOrKey": { 62 | // "value": "GEN-PASSWORD" 63 | // }, 64 | // "dnsLabelPrefix": { 65 | // "value": "GEN-UNIQUE" 66 | // } 67 | // } 68 | if parameters["$schema"] != nil { 69 | 70 | return parameters["parameters"].(map[string]interface{}) 71 | 72 | } 73 | return parameters 74 | } 75 | -------------------------------------------------------------------------------- /pkg/infrastructure/ARMTemplateShared/armTemplateShared_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package ARMTemplateShared 24 | 25 | import ( 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func TestGetParametersInStandardFormatWithSchema(t *testing.T) { 32 | parameters := map[string]interface{}{ 33 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 34 | "contentVersion": "1.0.0.0", 35 | "parameters": map[string]interface{}{ 36 | "adminUsername": map[string]interface{}{ 37 | "value": "GEN-UNIQUE", 38 | }, 39 | "adminPasswordOrKey": map[string]interface{}{ 40 | "value": "GEN-PASSWORD", 41 | }, 42 | "dnsLabelPrefix": map[string]interface{}{ 43 | "value": "GEN-UNIQUE", 44 | }, 45 | }, 46 | } 47 | 48 | expected := map[string]interface{}{ 49 | "adminUsername": map[string]interface{}{ 50 | "value": "GEN-UNIQUE", 51 | }, 52 | "adminPasswordOrKey": map[string]interface{}{ 53 | "value": "GEN-PASSWORD", 54 | }, 55 | "dnsLabelPrefix": map[string]interface{}{ 56 | "value": "GEN-UNIQUE", 57 | }, 58 | } 59 | 60 | result := GetParametersInStandardFormat(parameters) 61 | assert.Equal(t, expected, result) 62 | } 63 | 64 | func TestGetParametersInStandardFormatWithoutSchema(t *testing.T) { 65 | parameters := map[string]interface{}{ 66 | "adminUsername": map[string]interface{}{ 67 | "value": "GEN-UNIQUE", 68 | }, 69 | "adminPasswordOrKey": map[string]interface{}{ 70 | "value": "GEN-PASSWORD", 71 | }, 72 | "dnsLabelPrefix": map[string]interface{}{ 73 | "value": "GEN-UNIQUE", 74 | }, 75 | } 76 | 77 | expected := map[string]interface{}{ 78 | "adminUsername": map[string]interface{}{ 79 | "value": "GEN-UNIQUE", 80 | }, 81 | "adminPasswordOrKey": map[string]interface{}{ 82 | "value": "GEN-PASSWORD", 83 | }, 84 | "dnsLabelPrefix": map[string]interface{}{ 85 | "value": "GEN-UNIQUE", 86 | }, 87 | } 88 | 89 | result := GetParametersInStandardFormat(parameters) 90 | assert.Equal(t, expected, result) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/infrastructure/authorizationCheckers/terraform/fileManager.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package terraform 24 | 25 | import ( 26 | "encoding/json" 27 | "io" 28 | "os" 29 | 30 | "github.com/aliveticket/mpf/pkg/domain" 31 | log "github.com/sirupsen/logrus" 32 | ) 33 | 34 | func DoesTFFileExist(workingDir string, fileName string) bool { 35 | filePath := workingDir + "/" + fileName 36 | 37 | if _, err := os.Stat(filePath); err == nil { 38 | log.Infof("%s file exists \n", filePath) 39 | return true 40 | } 41 | log.Infof("%s file does not exist \n", filePath) 42 | return false 43 | } 44 | 45 | func CreateTFFile(workingDir string, fileName string) error { 46 | filePath := workingDir + "/" + fileName 47 | 48 | if _, err := os.Stat(filePath); err == nil { 49 | log.Infof("%s file already exists \n", filePath) 50 | return nil 51 | } 52 | 53 | _, err := os.Create(filePath) 54 | if err != nil { 55 | log.Warnf("error creating %s file: %s", filePath, err) 56 | return err 57 | } 58 | log.Infof("%s created file \n", filePath) 59 | return nil 60 | } 61 | 62 | func DeleteTFFile(workingDir string, fileName string) error { 63 | filePath := workingDir + "/" + fileName 64 | 65 | if _, err := os.Stat(filePath); err != nil { 66 | log.Infof("%s file does not exist \n", filePath) 67 | return nil 68 | } 69 | 70 | err := os.Remove(filePath) 71 | if err != nil { 72 | log.Warnf("error deleting %s file: %s", filePath, err) 73 | return err 74 | } 75 | log.Infof("%s deleted file \n", filePath) 76 | return nil 77 | } 78 | 79 | func doesEnteredDestroyPhaseStateFileExist(workingDir string, fileName string) bool { 80 | return DoesTFFileExist(workingDir, fileName) 81 | } 82 | 83 | func createEnteredDestroyPhaseStateFile(workingDir string, fileName string) error { 84 | return CreateTFFile(workingDir, fileName) 85 | } 86 | 87 | func deleteEnteredDestroyPhaseStateFile(workingDir string, fileName string) error { 88 | return DeleteTFFile(workingDir, fileName) 89 | } 90 | 91 | func saveResultAsJSON(rw io.ReadWriter, mpfResult domain.MPFResult) error { 92 | // serialize mpfREsult to json 93 | return json.NewEncoder(rw).Encode(mpfResult) 94 | } 95 | 96 | func loadResultFromJSON(r io.Reader) (*domain.MPFResult, error) { 97 | // deserialize json to mpfResult 98 | var mpfResult domain.MPFResult 99 | err := json.NewDecoder(r).Decode(&mpfResult) 100 | return &mpfResult, err 101 | } 102 | 103 | func LoadMPFResultFromFile(workingDir string, filename string) (*domain.MPFResult, error) { 104 | filePath := workingDir + "/" + filename 105 | file, err := os.Open(filePath) 106 | if err != nil { 107 | log.Warnf("error opening file for found permissions from failed run: %s", err) 108 | return nil, err 109 | } 110 | defer file.Close() 111 | 112 | return loadResultFromJSON(file) 113 | } 114 | 115 | // called for failed runs to save permissions to a file 116 | func SaveMPFResultsToFile(workingDir string, filename string, mpfResult domain.MPFResult) error { 117 | filePath := workingDir + "/" + filename 118 | file, err := os.Create(filePath) 119 | if err != nil { 120 | log.Warnf("error creating file for found permissions from failed run: %s", err) 121 | return err 122 | } 123 | defer file.Close() 124 | 125 | return saveResultAsJSON(file, mpfResult) 126 | 127 | } 128 | -------------------------------------------------------------------------------- /pkg/infrastructure/authorizationCheckers/terraform/fileManager_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package terraform 24 | 25 | import ( 26 | "bytes" 27 | "strings" 28 | "testing" 29 | 30 | "github.com/aliveticket/mpf/pkg/domain" 31 | ) 32 | 33 | func TestSaveResultAsJSON(t *testing.T) { 34 | tests := []struct { 35 | name string 36 | mpfResult domain.MPFResult 37 | wantErr bool 38 | wantOutput string 39 | }{ 40 | { 41 | name: "valid MPFResult", 42 | mpfResult: domain.MPFResult{ 43 | RequiredPermissions: map[string][]string{ 44 | "": []string{ 45 | "Microsoft.Authorization/roleAssignments/delete", 46 | "Microsoft.Authorization/roleAssignments/read", 47 | "Microsoft.Authorization/roleAssignments/write", 48 | }, 49 | }, 50 | }, 51 | wantErr: false, 52 | wantOutput: `{"RequiredPermissions":{"":["Microsoft.Authorization/roleAssignments/delete","Microsoft.Authorization/roleAssignments/read","Microsoft.Authorization/roleAssignments/write"]}}`, 53 | }, 54 | { 55 | name: "valid MPFResult with detailed permissions", 56 | mpfResult: domain.MPFResult{ 57 | RequiredPermissions: map[string][]string{ 58 | "": []string{ 59 | "Microsoft.ContainerService/managedClusters/read", 60 | "Microsoft.ContainerService/managedClusters/write", 61 | "Microsoft.Network/virtualNetworks/read", 62 | "Microsoft.Network/virtualNetworks/subnets/read", 63 | "Microsoft.Network/virtualNetworks/subnets/write", 64 | "Microsoft.Network/virtualNetworks/write", 65 | "Microsoft.Resources/deployments/read", 66 | "Microsoft.Resources/deployments/write", 67 | }, 68 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44/providers/Microsoft.ContainerService/managedClusters/azmpfakstestcluster": []string{ 69 | "Microsoft.ContainerService/managedClusters/read", 70 | "Microsoft.ContainerService/managedClusters/write", 71 | }, 72 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44/providers/Microsoft.Network/virtualNetworks/azmpfakstestvnet": []string{ 73 | "Microsoft.Network/virtualNetworks/read", 74 | "Microsoft.Network/virtualNetworks/write", 75 | }, 76 | "/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44/providers/Microsoft.Network/virtualNetworks/azmpfakstestvnet/subnets/azmpfakstestsubnet": []string{ 77 | "Microsoft.Network/virtualNetworks/subnets/read", 78 | "Microsoft.Network/virtualNetworks/subnets/write", 79 | }, 80 | }, 81 | }, 82 | wantErr: false, 83 | wantOutput: `{"RequiredPermissions":{"":["Microsoft.ContainerService/managedClusters/read","Microsoft.ContainerService/managedClusters/write","Microsoft.Network/virtualNetworks/read","Microsoft.Network/virtualNetworks/subnets/read","Microsoft.Network/virtualNetworks/subnets/write","Microsoft.Network/virtualNetworks/write","Microsoft.Resources/deployments/read","Microsoft.Resources/deployments/write"],"/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44/providers/Microsoft.ContainerService/managedClusters/azmpfakstestcluster":["Microsoft.ContainerService/managedClusters/read","Microsoft.ContainerService/managedClusters/write"],"/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44/providers/Microsoft.Network/virtualNetworks/azmpfakstestvnet":["Microsoft.Network/virtualNetworks/read","Microsoft.Network/virtualNetworks/write"],"/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44/providers/Microsoft.Network/virtualNetworks/azmpfakstestvnet/subnets/azmpfakstestsubnet":["Microsoft.Network/virtualNetworks/subnets/read","Microsoft.Network/virtualNetworks/subnets/write"]}}`, 84 | }, 85 | { 86 | name: "empty MPFResult", 87 | mpfResult: domain.MPFResult{ 88 | RequiredPermissions: map[string][]string{}, 89 | }, 90 | wantErr: false, 91 | wantOutput: `{"RequiredPermissions":{}}`, 92 | }, 93 | } 94 | 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | var buf bytes.Buffer 98 | err := saveResultAsJSON(&buf, tt.mpfResult) 99 | if (err != nil) != tt.wantErr { 100 | t.Errorf("saveResultAsJSON() error = %v, wantErr %v", err, tt.wantErr) 101 | return 102 | } 103 | if gotOutput := buf.String(); strings.TrimSpace(gotOutput) != strings.TrimSpace(tt.wantOutput) { 104 | t.Errorf("saveResultAsJSON() = %v, want %v", gotOutput, tt.wantOutput) 105 | } 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pkg/infrastructure/authorizationCheckers/terraform/resourceImportParser.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package terraform 24 | 25 | import ( 26 | "errors" 27 | "regexp" 28 | "strings" 29 | 30 | log "github.com/sirupsen/logrus" 31 | ) 32 | 33 | func GetAddressAndResourceIDFromExistingResourceError(existingResourceErr string) (map[string]string, error) { 34 | if existingResourceErr != "" && !strings.Contains(existingResourceErr, TFExistingResourceErrorMsg) { 35 | log.Infoln("Non existing resource error :", existingResourceErr) 36 | return nil, errors.New("Non existing resource error") 37 | } 38 | 39 | var resMap map[string]string = make(map[string]string) 40 | // var err error 41 | re := regexp.MustCompile(`Error: A resource with the ID "([^"]+)" already exists - to be managed via Terraform this resource needs to be imported into the State. Please see the resource documentation for "([^"]+)" for more information.\n\n with ([^,]+),`) 42 | // re := regexp.MustCompile(`Error: A resource with the ID "([^']+)" already exists - to be managed via Terraform this resource needs to be imported into the State. Please see the resource documentation for "([^']+)" for more information\.(.*) with ([^,]+),`) 43 | 44 | matches := re.FindAllStringSubmatch(existingResourceErr, -1) 45 | 46 | if len(matches) == 0 { 47 | return nil, errors.New("No matches found in 'existingResourceErrorMessage' error message") 48 | } 49 | 50 | for _, match := range matches { 51 | if len(match) == 4 { 52 | // resourceType := match[1] 53 | resID := match[1] 54 | addr := match[3] 55 | resMap[addr] = resID 56 | } 57 | } 58 | 59 | return resMap, nil 60 | } 61 | -------------------------------------------------------------------------------- /pkg/infrastructure/azureAPI/azureApiClient.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package azureAPI 24 | 25 | import ( 26 | "context" 27 | "time" 28 | 29 | "github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization" 30 | "github.com/Azure/azure-sdk-for-go/sdk/azcore" 31 | "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" 32 | "github.com/Azure/azure-sdk-for-go/sdk/azidentity" 33 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" 34 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" 35 | "github.com/Azure/go-autorest/autorest" 36 | "github.com/Azure/go-autorest/autorest/azure/auth" 37 | log "github.com/sirupsen/logrus" 38 | ) 39 | 40 | type AzureAPIClients struct { 41 | RoleAssignmentsClient authorization.RoleAssignmentsClient 42 | RoleAssignmentsDeletionClient *armauthorization.RoleAssignmentsClient 43 | // RoleDefinitionsClient authorization.RoleDefinitionsClient 44 | DeploymentsClient *armresources.DeploymentsClient 45 | ResourceGroupsClient *armresources.ResourceGroupsClient 46 | 47 | // Default CLI Creds 48 | DefaultCred *azidentity.DefaultAzureCredential 49 | defaultAPIBearerToken string 50 | defaultAPIBearerTokenLastCachedTime time.Time 51 | // SPCred *azidentity.ClientSecretCredential 52 | } 53 | 54 | const defaultTokenCacheDuration = 10 * time.Minute 55 | 56 | func NewAzureAPIClients(subscriptionID string) *AzureAPIClients { 57 | a := &AzureAPIClients{} 58 | err := a.SetApiClients(subscriptionID) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | return a 63 | } 64 | 65 | func getAuthorizer() (authorizer autorest.Authorizer, err error) { 66 | // Use the default Azure environment for authentication 67 | authorizer, err = auth.NewAuthorizerFromCLI() 68 | if err != nil { 69 | return nil, err 70 | } 71 | return authorizer, nil 72 | } 73 | 74 | type TokenProvider interface { 75 | GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) 76 | } 77 | 78 | func (a *AzureAPIClients) getBearerToken(tp TokenProvider) (bearerToken string, err error) { 79 | opts := policy.TokenRequestOptions{Scopes: []string{"https://management.azure.com/.default"}} 80 | tok, err := tp.GetToken(context.Background(), opts) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | return tok.Token, nil 86 | } 87 | 88 | func (a *AzureAPIClients) SetApiClients(subscriptionId string) error { 89 | authorizer, err := getAuthorizer() 90 | if err != nil { 91 | return err 92 | } 93 | 94 | a.DefaultCred, err = azidentity.NewDefaultAzureCredential(nil) 95 | if err != nil { 96 | // log.Fatal(err) 97 | log.Fatal(err) 98 | } 99 | 100 | // Set RoleAssignmentsClient 101 | a.RoleAssignmentsClient = authorization.NewRoleAssignmentsClient(subscriptionId) 102 | a.RoleAssignmentsClient.Authorizer = authorizer 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | 107 | roleAssignmentsDeletionClientFactory, err := armauthorization.NewClientFactory(subscriptionId, a.DefaultCred, nil) 108 | if err != nil { 109 | log.Fatalf("failed to create role assignments deletion client factory: %v", err) 110 | } 111 | 112 | a.RoleAssignmentsDeletionClient = roleAssignmentsDeletionClientFactory.NewRoleAssignmentsClient() 113 | 114 | // Set RoleDefinitionsClient 115 | // a.RoleDefinitionsClient = authorization.NewRoleDefinitionsClient(subscriptionId) 116 | // a.RoleDefinitionsClient.Authorizer = authorizer 117 | 118 | resourcesClientFactory, err := armresources.NewClientFactory(subscriptionId, a.DefaultCred, nil) 119 | if err != nil { 120 | log.Fatal(err) 121 | } 122 | 123 | // Set DeploymentsClient 124 | a.DeploymentsClient = resourcesClientFactory.NewDeploymentsClient() 125 | 126 | // Set ResourceGroupsClient 127 | a.ResourceGroupsClient, err = armresources.NewResourceGroupsClient(subscriptionId, a.DefaultCred, nil) 128 | if err != nil { 129 | log.Fatal(err) 130 | } 131 | 132 | return nil 133 | 134 | } 135 | 136 | func (a *AzureAPIClients) GetSPBearerToken(tenantID, spClientID, spClientSecret string) (string, error) { 137 | // Get the Service Principal creds 138 | spCred, err := azidentity.NewClientSecretCredential(tenantID, spClientID, spClientSecret, nil) 139 | if err != nil { 140 | log.Error(err) 141 | return "", err 142 | } 143 | 144 | bearerToken, err := a.getBearerToken(spCred) 145 | if err != nil { 146 | log.Error(err) 147 | return "", err 148 | } 149 | 150 | return bearerToken, nil 151 | 152 | } 153 | 154 | func (a *AzureAPIClients) GetDefaultAPIBearerToken() (bearerToken string, err error) { 155 | 156 | if a.defaultAPIBearerToken == "" || time.Since(a.defaultAPIBearerTokenLastCachedTime) > defaultTokenCacheDuration { 157 | bearerToken, err = a.getBearerToken(a.DefaultCred) 158 | if err != nil { 159 | return "", err 160 | } 161 | 162 | a.defaultAPIBearerToken = bearerToken 163 | a.defaultAPIBearerTokenLastCachedTime = time.Now() 164 | log.Infoln("Default API Bearer Token set") 165 | } 166 | 167 | return a.defaultAPIBearerToken, nil 168 | } 169 | 170 | // func (m *MinPermFinder) RefreshSPAPIAccessBearerToken() error { 171 | // // Get the bearer token for the API access 172 | 173 | // bearerToken, err := m.getBearerToken(m.SPCred) 174 | // if err != nil { 175 | // return err 176 | // } 177 | 178 | // m.SPCredBearerToken = bearerToken 179 | // return nil 180 | // } 181 | 182 | // func (m *MinPermFinder) CreateCustomRoleWithInitialScopeAndPermissions() error { 183 | 184 | // initialScope := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", m.SubscriptionID, m.ResourceGroupName) 185 | // // initialScope := fmt.Sprintf("/subscriptions/%s", m.SubscriptionID) 186 | 187 | // roleDefinition := authorization.RoleDefinition{ 188 | // RoleDefinitionProperties: &authorization.RoleDefinitionProperties{ 189 | // RoleName: &m.RoleDefinitionName, 190 | // Description: &m.RoleDefinitionName, 191 | // AssignableScopes: &[]string{ 192 | // initialScope, 193 | // }, 194 | // Permissions: &[]authorization.Permission{ 195 | // { 196 | // Actions: to.StringSlicePtr([]string{ 197 | // // "Microsoft.Resources/deployments/read", 198 | // // "Microsoft.Resources/deployments/write", 199 | // }), 200 | // }, 201 | // }, 202 | // }, 203 | // } 204 | 205 | // // Create the custom role 206 | // _, err := m.RoleDefinitionsClient.CreateOrUpdate(m.Ctx, initialScope, m.RoleDefinitionID, roleDefinition) 207 | // return err 208 | // } 209 | -------------------------------------------------------------------------------- /pkg/infrastructure/mpfSharedUtils/json.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package mpfSharedUtils 24 | 25 | import ( 26 | "encoding/json" 27 | "os" 28 | ) 29 | 30 | func ReadJson(path string) (map[string]interface{}, error) { 31 | templateFile, err := os.ReadFile(path) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | template := make(map[string]interface{}) 37 | if err := json.Unmarshal(templateFile, &template); err != nil { 38 | return nil, err 39 | } 40 | 41 | return template, nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/infrastructure/mpfSharedUtils/json_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package mpfSharedUtils 24 | 25 | import ( 26 | "encoding/json" 27 | "os" 28 | "testing" 29 | 30 | "github.com/stretchr/testify/assert" 31 | ) 32 | 33 | func TestReadJson(t *testing.T) { 34 | // Create a temporary JSON file for testing 35 | tempFile, err := os.CreateTemp("", "test.json") 36 | assert.Nil(t, err) 37 | defer os.Remove(tempFile.Name()) 38 | 39 | // Define a sample JSON content 40 | jsonContent := `{"name": "John Doe", "age": 30}` 41 | 42 | // Write the JSON content to the temporary file 43 | err = os.WriteFile(tempFile.Name(), []byte(jsonContent), 0644) 44 | assert.Nil(t, err) 45 | 46 | // Call the ReadJson function with the temporary file path 47 | result, err := ReadJson(tempFile.Name()) 48 | 49 | // Assert that there is no error 50 | assert.Nil(t, err) 51 | 52 | // Assert that the result matches the expected JSON content 53 | expected := make(map[string]interface{}) 54 | err = json.Unmarshal([]byte(jsonContent), &expected) 55 | assert.Nil(t, err) 56 | assert.Equal(t, expected, result) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/infrastructure/mpfSharedUtils/randomString.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package mpfSharedUtils 24 | 25 | import ( 26 | "crypto/rand" 27 | "math/big" 28 | ) 29 | 30 | func GenerateRandomString(length int) string { 31 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 32 | charsetLength := big.NewInt(int64(len(charset))) 33 | 34 | randomString := make([]byte, length) 35 | for i := range randomString { 36 | randomIndex, _ := rand.Int(rand.Reader, charsetLength) 37 | randomString[i] = charset[randomIndex.Int64()] 38 | } 39 | 40 | return string(randomString) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/infrastructure/resourceGroupManager/defaultResourceGroupManager.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package resourcegroupmanager 24 | 25 | import ( 26 | "context" 27 | 28 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" 29 | azureAPI "github.com/aliveticket/mpf/pkg/infrastructure/azureAPI" 30 | ) 31 | 32 | type RGManager struct { 33 | rgAPIClient *armresources.ResourceGroupsClient 34 | } 35 | 36 | func NewResourceGroupManager(subscriptionID string) *RGManager { 37 | azAPIClient := azureAPI.NewAzureAPIClients(subscriptionID) 38 | return &RGManager{ 39 | rgAPIClient: azAPIClient.ResourceGroupsClient, 40 | } 41 | } 42 | 43 | func (r *RGManager) DeleteResourceGroup(ctx context.Context, rgName string) error { 44 | 45 | _, err := r.rgAPIClient.BeginDelete(ctx, rgName, nil) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | return nil 51 | } 52 | 53 | // method to create resource group 54 | func (r *RGManager) CreateResourceGroup(ctx context.Context, rgName string, location string) error { 55 | 56 | rgParams := armresources.ResourceGroup{ 57 | Location: &location, 58 | Name: &rgName, 59 | } 60 | 61 | // create resource group 62 | _, err := r.rgAPIClient.CreateOrUpdate(ctx, rgName, rgParams, nil) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/presentation/defaultFormatter.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package presentation 24 | 25 | import ( 26 | "fmt" 27 | "io" 28 | "sort" 29 | 30 | log "github.com/sirupsen/logrus" 31 | ) 32 | 33 | func (d *displayConfig) displayText(w io.Writer) error { 34 | 35 | sm := d.result.RequiredPermissions 36 | if len(sm) == 0 { 37 | fmt.Println("No permissions required") 38 | return nil 39 | } 40 | 41 | log.Debugf("All permissions required: %v", sm) 42 | 43 | defaultPerms := sm[d.displayOptions.SubscriptionID] 44 | 45 | // sort permissions 46 | // defaultPerms = getUniqueSlice(defaultPerms) 47 | sort.Strings(defaultPerms) 48 | 49 | // print permissions for default scope 50 | fmt.Println("------------------------------------------------------------------------------------------------------------------------------------------") 51 | fmt.Println("Permissions Required:") 52 | fmt.Println("------------------------------------------------------------------------------------------------------------------------------------------") 53 | for _, perm := range defaultPerms { 54 | fmt.Println(perm) 55 | } 56 | fmt.Println("------------------------------------------------------------------------------------------------------------------------------------------") 57 | fmt.Println() 58 | 59 | if !d.displayOptions.ShowDetailedOutput { 60 | return nil 61 | } 62 | 63 | fmt.Println() 64 | fmt.Println("Break down of permissions by different resource types:") 65 | fmt.Println() 66 | 67 | // print permissions for other scopes 68 | for scope, perms := range sm { 69 | if scope == d.displayOptions.SubscriptionID { 70 | continue 71 | } 72 | 73 | // perms = getUniqueSlice(perms) 74 | sort.Strings(perms) 75 | 76 | fmt.Printf("Permissions required for %s: \n", scope) 77 | for _, perm := range perms { 78 | fmt.Printf("%s\n", perm) 79 | } 80 | fmt.Println("--------------") 81 | fmt.Println() 82 | fmt.Println() 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/presentation/jsonFormatter.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package presentation 24 | 25 | import ( 26 | "encoding/json" 27 | "io" 28 | "log" 29 | ) 30 | 31 | func (d *displayConfig) displayJSON(w io.Writer) error { 32 | jsonBytes, err := json.Marshal(d.result.RequiredPermissions) 33 | if err != nil { 34 | log.Fatalf("Error converting output to JSON :%v \n", err) 35 | } 36 | // fmt.Println(string(jsonBytes)) 37 | _, err = w.Write(jsonBytes) 38 | if err != nil { 39 | return err 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/presentation/resultPresenter.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package presentation 24 | 25 | import ( 26 | "io" 27 | 28 | "github.com/aliveticket/mpf/pkg/domain" 29 | ) 30 | 31 | type DisplayOptions struct { 32 | ShowDetailedOutput bool 33 | JSONOutput bool 34 | SubscriptionID string 35 | } 36 | 37 | // type ResultDisplayer interface { 38 | // DisplayMPFResult(w io.Writer ,result domain.MPFResult, options displayOptions) error 39 | // } 40 | 41 | type displayConfig struct { 42 | result domain.MPFResult 43 | displayOptions DisplayOptions 44 | } 45 | 46 | func NewMPFResultDisplayer(result domain.MPFResult, options DisplayOptions) *displayConfig { 47 | return &displayConfig{ 48 | result: result, 49 | displayOptions: options, 50 | } 51 | } 52 | 53 | func (d *displayConfig) DisplayResult(w io.Writer) error { 54 | if d.displayOptions.JSONOutput { 55 | return d.displayJSON(w) 56 | } 57 | return d.displayText(w) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/usecase/deploymentAuthorizationCheckerCleaner.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package usecase 24 | 25 | import "github.com/aliveticket/mpf/pkg/domain" 26 | 27 | type DeploymentAuthorizationChecker interface { 28 | 29 | // Check if the user has the required permissions to deploy the template 30 | // If Authorization Error is received the authorization error message string is returned, and error is nil 31 | // If string is empty and error is not nill, then non authorization error is received 32 | // If string is empty and error is nil, then authorization is successful 33 | GetDeploymentAuthorizationErrors(mpfCoreConfig domain.MPFConfig) (string, error) 34 | } 35 | 36 | type DeploymentCleaner interface { 37 | CleanDeployment(mpfCoreConfig domain.MPFConfig) error 38 | } 39 | 40 | type DeploymentAuthorizationCheckerCleaner interface { 41 | DeploymentAuthorizationChecker 42 | DeploymentCleaner 43 | } 44 | -------------------------------------------------------------------------------- /pkg/usecase/resourceGroupManager.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package usecase 24 | 25 | import "context" 26 | 27 | type ResourceGroupManager interface { 28 | CreateResourceGroup(ctx context.Context, rgName, location string) error 29 | DeleteResourceGroup(ctx context.Context, rgName string) error 30 | } 31 | -------------------------------------------------------------------------------- /pkg/usecase/servicePrincipalRoleAssignmentManager.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE 22 | 23 | package usecase 24 | 25 | import ( 26 | "context" 27 | 28 | "github.com/aliveticket/mpf/pkg/domain" 29 | ) 30 | 31 | type ServicePrincipalAssignmentModifier interface { 32 | DetachRolesFromSP(ctx context.Context, subscription string, SPOBjectID string, role domain.Role) error 33 | AssignRoleToSP(subscription string, SPOBjectID string, role domain.Role) error 34 | } 35 | 36 | type CustomRoleCreatorModifier interface { 37 | CreateUpdateCustomRole(subscription string, role domain.Role, permissions []string) error 38 | DeleteCustomRole(subscription string, role domain.Role) error 39 | } 40 | 41 | type ServicePrincipalRolemAssignmentManager interface { 42 | ServicePrincipalAssignmentModifier 43 | CustomRoleCreatorModifier 44 | } 45 | -------------------------------------------------------------------------------- /samples/bicep/aks-private-subnet-invalid-params.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "value": "eastus" 7 | }, 8 | "vnetNames": { 9 | "value": "myVNet" 10 | }, 11 | "subnetName": { 12 | "value": "mySubnet" 13 | }, 14 | "clusterName": { 15 | "value": "myAKSCluster" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /samples/bicep/aks-private-subnet-params.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "value": "eastus" 7 | }, 8 | "vnetName": { 9 | "value": "myVNet" 10 | }, 11 | "subnetName": { 12 | "value": "mySubnet" 13 | }, 14 | "clusterName": { 15 | "value": "myAKSCluster" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /samples/bicep/aks-private-subnet.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param clusterName string = 'myAKSCluster' 3 | param vnetName string = 'myVNet' 4 | param subnetName string = 'mySubnet' 5 | 6 | resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = { 7 | name: vnetName 8 | location: location 9 | properties: { 10 | addressSpace: { 11 | addressPrefixes: [ 12 | '10.0.0.0/16' 13 | ] 14 | } 15 | } 16 | } 17 | 18 | resource subnet 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' = { 19 | parent: vnet 20 | name: subnetName 21 | properties: { 22 | addressPrefix: '10.0.0.0/24' 23 | privateEndpointNetworkPolicies: 'Disabled' 24 | privateLinkServiceNetworkPolicies: 'Disabled' 25 | } 26 | } 27 | 28 | resource aksCluster 'Microsoft.ContainerService/managedClusters@2021-07-01' = { 29 | name: clusterName 30 | location: location 31 | properties: { 32 | kubernetesVersion: '1.21.2' 33 | dnsPrefix: clusterName 34 | enableRBAC: true 35 | networkProfile: { 36 | networkPlugin: 'azure' 37 | networkMode: 'private' 38 | loadBalancerSku: 'standard' 39 | networkPolicy: 'calico' 40 | podCidr: '10.244.0.0/16' 41 | serviceCidr: '10.245.0.0/16' 42 | dockerBridgeCidr: '172.17.0.1/16' 43 | outboundType: 'loadBalancer' 44 | loadBalancerProfile: { 45 | managedOutboundIPs: { 46 | count: 1 47 | } 48 | } 49 | } 50 | agentPoolProfiles: [ 51 | { 52 | name: 'agentpool' 53 | count: 1 54 | vmSize: 'Standard_DS2_v2' 55 | osType: 'Linux' 56 | osDiskSizeGB: 30 57 | vnetSubnetID: subnet.id 58 | } 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /samples/bicep/invalid-bicep.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param clusterName string = 'myAKSCluster' 3 | param vnetName string = 'myVNet' 4 | param subnetName string = 'mySubnet' 5 | 6 | resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = { 7 | // names and locations are incorrect, this file should result in invalidTemplate error 8 | names: vnetName 9 | locations: location 10 | properties: { 11 | addressSpace: { 12 | addressPrefixes: [ 13 | '10.0.0.0/16' 14 | ] 15 | } 16 | } 17 | } 18 | 19 | resource subnet 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' = { 20 | parent: vnet 21 | name: subnetName 22 | properties: { 23 | addressPrefix: '10.0.0.0/24' 24 | privateEndpointNetworkPolicies: 'Disabled' 25 | privateLinkServiceNetworkPolicies: 'Disabled' 26 | } 27 | } 28 | 29 | resource aksCluster 'Microsoft.ContainerService/managedClusters@2021-07-01' = { 30 | name: clusterName 31 | location: location 32 | properties: { 33 | kubernetesVersion: '1.21.2' 34 | dnsPrefix: clusterName 35 | enableRBAC: true 36 | networkProfile: { 37 | networkPlugin: 'azure' 38 | networkMode: 'private' 39 | loadBalancerSku: 'standard' 40 | networkPolicy: 'calico' 41 | podCidr: '10.244.0.0/16' 42 | serviceCidr: '10.245.0.0/16' 43 | dockerBridgeCidr: '172.17.0.1/16' 44 | outboundType: 'loadBalancer' 45 | loadBalancerProfile: { 46 | managedOutboundIPs: { 47 | count: 1 48 | } 49 | } 50 | } 51 | agentPoolProfiles: [ 52 | { 53 | name: 'agentpool' 54 | count: 1 55 | vmSize: 'Standard_DS2_v2' 56 | osType: 'Linux' 57 | osDiskSizeGB: 30 58 | vnetSubnetID: subnet.id 59 | } 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /samples/bicep/subscription-scope-create-rg-params.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/bicep/subscription-scope-create-rg.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @description('Generates a stable unique string for the resource group name') 4 | var randomString = uniqueString(subscription().id, 'resourceGroupNameSeed') 5 | 6 | @description('Resource group name') 7 | var resourceGroupName = 'rg-${take(randomString, 13)}' 8 | 9 | @description('location of the resource group') 10 | var location = 'eastus2' 11 | 12 | // Module to deploy the resource group at the subscription scope 13 | resource coreResourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { 14 | name: resourceGroupName 15 | location: location 16 | } 17 | -------------------------------------------------------------------------------- /samples/templates/aks-invalid-params.json: -------------------------------------------------------------------------------- 1 | { 2 | "dnsPrefixIncorrect": { 3 | "value": "manitstdnspfx32" 4 | }, 5 | "sshRSAPublicKeyIncorrect": { 6 | "value": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKoYj0DwFvl8R2v7EV2VLOz+t7sPGnPXF5rR/BoTww8TkgVPaHuEd73r8QebLrTZWhpthUMNtLDzd/gv4uN0+N2/O1IAr0CJ5NAhHZHX1EddMGU3SMjcnLh3p8+iLXkre2PDRYF4J1wgsHmUUE8wTyIvzVRze5yAKXE9/6oi13l8sd2lWfAkHP3/c3LTFhertrXRZpG37XDCCHzzmjfVP8syLwFpZeIvRYlQ3IvlOZALyB5rW+FRyC01ZM9RSuZ8FyY8lPDCqkDw8ZoNsZrHodwtOlBRwYOTOO6BfJASZXD7VRv+MnpRUiv18TxNSVAt14nbJTJ4kVi9H8ch/K9Q2h" 7 | }, 8 | "linuxAdminUsernameIncorrect": { 9 | "value": "manitestvmadminuser" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/templates/aks-invalid-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.9.1.41621", 8 | "templateHash": "2637152180661081755" 9 | } 10 | }, 11 | "parameters": { 12 | "clusterName": { 13 | "type": "string", 14 | "defaultValue": "aks101cluster", 15 | "metadata": { 16 | "description": "The name of the Managed Cluster resource." 17 | } 18 | }, 19 | "location": { 20 | "type": "string", 21 | "defaultValue": "[resourceGroup().location]", 22 | "metadata": { 23 | "description": "The location of the Managed Cluster resource." 24 | } 25 | }, 26 | "dnsPrefix": { 27 | "type": "string", 28 | "metadata": { 29 | "description": "Optional DNS prefix to use with hosted Kubernetes API server FQDN." 30 | } 31 | }, 32 | "osDiskSizeGB": { 33 | "type": "int", 34 | "defaultValue": 0, 35 | "maxValue": 1023, 36 | "minValue": 0, 37 | "metadata": { 38 | "description": "Disk size (in GB) to provision for each of the agent pool nodes. This value ranges from 0 to 1023. Specifying 0 will apply the default disk size for that agentVMSize." 39 | } 40 | }, 41 | "agentCount": { 42 | "type": "int", 43 | "defaultValue": 3, 44 | "maxValue": 50, 45 | "minValue": 1, 46 | "metadata": { 47 | "description": "The number of nodes for the cluster." 48 | } 49 | }, 50 | "agentVMSize": { 51 | "type": "string", 52 | "defaultValue": "standard_d2s_v3", 53 | "metadata": { 54 | "description": "The size of the Virtual Machine." 55 | } 56 | }, 57 | "linuxAdminUsername": { 58 | "type": "string", 59 | "metadata": { 60 | "description": "User name for the Linux Virtual Machines." 61 | } 62 | }, 63 | "sshRSAPublicKey": { 64 | "type": "string", 65 | "metadata": { 66 | "description": "Configure all linux machines with the SSH RSA public key string. Your key should include three parts, for example 'ssh-rsa AAAAB...snip...UcyupgH azureuser@linuxvm'" 67 | } 68 | } 69 | }, 70 | "resource": [ 71 | { 72 | "type": "Microsoft.ContainerService/managedClusters", 73 | "apiVersion": "2022-05-02-preview", 74 | "name": "[parameters('clusterName')]", 75 | "location": "[parameters('location')]", 76 | "identity": { 77 | "type": "SystemAssigned" 78 | }, 79 | "properties": { 80 | "dnsPrefix": "[parameters('dnsPrefix')]", 81 | "agentPoolProfiles": [ 82 | { 83 | "name": "agentpool", 84 | "osDiskSizeGB": "[parameters('osDiskSizeGB')]", 85 | "count": "[parameters('agentCount')]", 86 | "vmSize": "[parameters('agentVMSize')]", 87 | "osType": "Linux", 88 | "mode": "System" 89 | } 90 | ], 91 | "linuxProfile": { 92 | "adminUsername": "[parameters('linuxAdminUsername')]", 93 | "ssh": { 94 | "publicKeys": [ 95 | { 96 | "keyData": "[parameters('sshRSAPublicKey')]" 97 | } 98 | ] 99 | } 100 | } 101 | } 102 | } 103 | ], 104 | "outputs": { 105 | "controlPlaneFQDN": { 106 | "type": "string", 107 | "value": "[reference(resourceId('Microsoft.ContainerService/managedClusters', parameters('clusterName'))).fqdn]" 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /samples/templates/aks-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "dnsPrefix": { 3 | "value": "manitstdnspfx32" 4 | }, 5 | "sshRSAPublicKey": { 6 | "value": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKoYj0DwFvl8R2v7EV2VLOz+t7sPGnPXF5rR/BoTww8TkgVPaHuEd73r8QebLrTZWhpthUMNtLDzd/gv4uN0+N2/O1IAr0CJ5NAhHZHX1EddMGU3SMjcnLh3p8+iLXkre2PDRYF4J1wgsHmUUE8wTyIvzVRze5yAKXE9/6oi13l8sd2lWfAkHP3/c3LTFhertrXRZpG37XDCCHzzmjfVP8syLwFpZeIvRYlQ3IvlOZALyB5rW+FRyC01ZM9RSuZ8FyY8lPDCqkDw8ZoNsZrHodwtOlBRwYOTOO6BfJASZXD7VRv+MnpRUiv18TxNSVAt14nbJTJ4kVi9H8ch/K9Q2h" 7 | }, 8 | "linuxAdminUsername": { 9 | "value": "manitestvmadminuser" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/templates/aks-private-subnet-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "clusterName": { 3 | "value": "azmpfakstestcluster" 4 | }, 5 | "vnetName": { 6 | "value": "azmpfakstestvnet" 7 | }, 8 | "subnetName": { 9 | "value": "azmpfakstestsubnet" 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /samples/templates/aks-private-subnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "clusterName": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "Name of the AKS cluster." 9 | } 10 | }, 11 | "vnetName": { 12 | "type": "string", 13 | "metadata": { 14 | "description": "Name of the virtual network." 15 | } 16 | }, 17 | "subnetName": { 18 | "type": "string", 19 | "metadata": { 20 | "description": "Name of the subnet." 21 | } 22 | }, 23 | "vnetAddressPrefix": { 24 | "type": "string", 25 | "defaultValue": "10.13.0.0/16", 26 | "metadata": { 27 | "description": "Address prefix for the virtual network." 28 | } 29 | }, 30 | "subnetAddressPrefix": { 31 | "type": "string", 32 | "defaultValue": "10.13.2.0/24", 33 | "metadata": { 34 | "description": "Address prefix for the subnet." 35 | } 36 | } 37 | }, 38 | "variables": { 39 | "location": "[resourceGroup().location]" 40 | }, 41 | "resources": [ 42 | { 43 | "type": "Microsoft.Network/virtualNetworks", 44 | "apiVersion": "2017-06-01", 45 | "name": "[parameters('vnetName')]", 46 | "location": "[variables('location')]", 47 | "properties": { 48 | "addressSpace": { 49 | "addressPrefixes": [ 50 | "[parameters('vnetAddressPrefix')]" 51 | ] 52 | } 53 | } 54 | }, 55 | { 56 | "type": "Microsoft.Network/virtualNetworks/subnets", 57 | "apiVersion": "2017-06-01", 58 | "name": "[concat(parameters('vnetName'), '/', parameters('subnetName'))]", 59 | "location": "[variables('location')]", 60 | "dependsOn": [ 61 | "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]" 62 | ], 63 | "properties": { 64 | "addressPrefix": "[parameters('subnetAddressPrefix')]" 65 | } 66 | }, 67 | { 68 | "type": "Microsoft.ContainerService/managedClusters", 69 | "apiVersion": "2021-07-01", 70 | "name": "[parameters('clusterName')]", 71 | "location": "[variables('location')]", 72 | "dependsOn": [ 73 | "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('subnetName'))]" 74 | ], 75 | "properties": { 76 | "kubernetesVersion": "1.21.2", 77 | "dnsPrefix": "[parameters('clusterName')]", 78 | "agentPoolProfiles": [ 79 | { 80 | "name": "agentpool", 81 | "count": 3, 82 | "vmSize": "Standard_D2_v2", 83 | "osType": "Linux", 84 | "osDiskSizeGB": 30, 85 | "vnetSubnetID": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('subnetName'))]" 86 | } 87 | ], 88 | "networkProfile": { 89 | "networkPlugin": "azure", 90 | "loadBalancerSku": "standard", 91 | "networkPolicy": "azure" 92 | } 93 | } 94 | } 95 | ] 96 | } -------------------------------------------------------------------------------- /samples/templates/aks.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.9.1.41621", 8 | "templateHash": "2637152180661081755" 9 | } 10 | }, 11 | "parameters": { 12 | "clusterName": { 13 | "type": "string", 14 | "defaultValue": "aks101cluster", 15 | "metadata": { 16 | "description": "The name of the Managed Cluster resource." 17 | } 18 | }, 19 | "location": { 20 | "type": "string", 21 | "defaultValue": "[resourceGroup().location]", 22 | "metadata": { 23 | "description": "The location of the Managed Cluster resource." 24 | } 25 | }, 26 | "dnsPrefix": { 27 | "type": "string", 28 | "metadata": { 29 | "description": "Optional DNS prefix to use with hosted Kubernetes API server FQDN." 30 | } 31 | }, 32 | "osDiskSizeGB": { 33 | "type": "int", 34 | "defaultValue": 0, 35 | "maxValue": 1023, 36 | "minValue": 0, 37 | "metadata": { 38 | "description": "Disk size (in GB) to provision for each of the agent pool nodes. This value ranges from 0 to 1023. Specifying 0 will apply the default disk size for that agentVMSize." 39 | } 40 | }, 41 | "agentCount": { 42 | "type": "int", 43 | "defaultValue": 3, 44 | "maxValue": 50, 45 | "minValue": 1, 46 | "metadata": { 47 | "description": "The number of nodes for the cluster." 48 | } 49 | }, 50 | "agentVMSize": { 51 | "type": "string", 52 | "defaultValue": "standard_d2s_v3", 53 | "metadata": { 54 | "description": "The size of the Virtual Machine." 55 | } 56 | }, 57 | "linuxAdminUsername": { 58 | "type": "string", 59 | "metadata": { 60 | "description": "User name for the Linux Virtual Machines." 61 | } 62 | }, 63 | "sshRSAPublicKey": { 64 | "type": "string", 65 | "metadata": { 66 | "description": "Configure all linux machines with the SSH RSA public key string. Your key should include three parts, for example 'ssh-rsa AAAAB...snip...UcyupgH azureuser@linuxvm'" 67 | } 68 | } 69 | }, 70 | "resources": [ 71 | { 72 | "type": "Microsoft.ContainerService/managedClusters", 73 | "apiVersion": "2022-05-02-preview", 74 | "name": "[parameters('clusterName')]", 75 | "location": "[parameters('location')]", 76 | "identity": { 77 | "type": "SystemAssigned" 78 | }, 79 | "properties": { 80 | "dnsPrefix": "[parameters('dnsPrefix')]", 81 | "agentPoolProfiles": [ 82 | { 83 | "name": "agentpool", 84 | "osDiskSizeGB": "[parameters('osDiskSizeGB')]", 85 | "count": "[parameters('agentCount')]", 86 | "vmSize": "[parameters('agentVMSize')]", 87 | "osType": "Linux", 88 | "mode": "System" 89 | } 90 | ], 91 | "linuxProfile": { 92 | "adminUsername": "[parameters('linuxAdminUsername')]", 93 | "ssh": { 94 | "publicKeys": [ 95 | { 96 | "keyData": "[parameters('sshRSAPublicKey')]" 97 | } 98 | ] 99 | } 100 | } 101 | } 102 | } 103 | ], 104 | "outputs": { 105 | "controlPlaneFQDN": { 106 | "type": "string", 107 | "value": "[reference(resourceId('Microsoft.ContainerService/managedClusters', parameters('clusterName'))).fqdn]" 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /samples/templates/blank-params.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliveticket/mpf/333389be98abd73796561e623d301a095bd4a2cf/samples/templates/blank-params.json -------------------------------------------------------------------------------- /samples/templates/blank-template.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliveticket/mpf/333389be98abd73796561e623d301a095bd4a2cf/samples/templates/blank-template.json -------------------------------------------------------------------------------- /samples/templates/empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.9.1.41621", 8 | "templateHash": "2637152180661081755" 9 | } 10 | }, 11 | "resources": [ 12 | ] 13 | } -------------------------------------------------------------------------------- /samples/templates/multi-resource-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "aksClusterAdminUsername": { 3 | "value": "manitestcladuser" 4 | }, 5 | "aksClusterSshPublicKey": { 6 | "value": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKoYj0DwFvl8R2v7EV2VLOz+t7sPGnPXF5rR/BoTww8TkgVPaHuEd73r8QebLrTZWhpthUMNtLDzd/gv4uN0+N2/O1IAr0CJ5NAhHZHX1EddMGU3SMjcnLh3p8+iLXkre2PDRYF4J1wgsHmUUE8wTyIvzVRze5yAKXE9/6oi13l8sd2lWfAkHP3/c3LTFhertrXRZpG37XDCCHzzmjfVP8syLwFpZeIvRYlQ3IvlOZALyB5rW+FRyC01ZM9RSuZ8FyY8lPDCqkDw8ZoNsZrHodwtOlBRwYOTOO6BfJASZXD7VRv+MnpRUiv18TxNSVAt14nbJTJ4kVi9H8ch/K9Q2h" 7 | }, 8 | "logAnalyticsWorkspaceName": { 9 | "value": "mantmplawsp131" 10 | }, 11 | "vmAdminUsername": { 12 | "value": "manitestcladuser" 13 | }, 14 | "vmAdminPasswordOrKey": { 15 | "value": "27B61244-5594-4283-92C0-B5E8F5CEFDB3" 16 | } 17 | } -------------------------------------------------------------------------------- /samples/templates/subscription-scope-create-rg-params.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/templates/subscription-scope-create-rg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | 5 | "variables": { 6 | "randomString": "[uniqueString(subscription().id, 'resourceGroupNameSeed')]", 7 | "resourceGroupName": "[format('rg-{0}', take(variables('randomString'), 13))]", 8 | "location": "eastus2" 9 | }, 10 | "resources": [ 11 | { 12 | "type": "Microsoft.Resources/resourceGroups", 13 | "apiVersion": "2021-04-01", 14 | "name": "[variables('resourceGroupName')]", 15 | "location": "[variables('location')]" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /samples/terraform/aci/dev.vars.tfvars: -------------------------------------------------------------------------------- 1 | 2 | 3 | location = "westus" 4 | postfix = "7422" -------------------------------------------------------------------------------- /samples/terraform/aci/main.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | terraform { 5 | 6 | } 7 | 8 | provider "azurerm" { 9 | features {} 10 | skip_provider_registration = "true" 11 | } 12 | 13 | resource "random_id" "rg" { 14 | byte_length = 8 15 | } 16 | 17 | resource "random_integer" "rnd_num" { 18 | min = 10000 19 | max = 30000 20 | } 21 | 22 | resource "azurerm_resource_group" "rg" { 23 | name = "rg-${random_id.rg.hex}" 24 | location = var.location 25 | } 26 | 27 | 28 | resource "azurerm_container_group" "aci" { 29 | name = "aci${random_integer.rnd_num.result}" 30 | location = azurerm_resource_group.rg.location 31 | resource_group_name = azurerm_resource_group.rg.name 32 | 33 | ip_address_type = "Public" 34 | dns_name_label = "aci${random_integer.rnd_num.result}" 35 | os_type = "Linux" 36 | 37 | container { 38 | name = "hello-world" 39 | image = "mcr.microsoft.com/azuredocs/aci-helloworld:latest" 40 | cpu = "0.5" 41 | memory = "1.5" 42 | 43 | ports { 44 | port = 443 45 | protocol = "TCP" 46 | } 47 | } 48 | 49 | tags = { 50 | Environment = "Development" 51 | } 52 | } -------------------------------------------------------------------------------- /samples/terraform/aci/output.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | output "resource_group_name" { 4 | value = azurerm_resource_group.rg.name 5 | } 6 | 7 | output "ip_address" { 8 | value = azurerm_container_group.aci.ip_address 9 | } 10 | 11 | output "fqdn" { 12 | value = azurerm_container_group.aci.fqdn 13 | } 14 | 15 | output "container_instance_name" { 16 | value = azurerm_container_group.aci.name 17 | } -------------------------------------------------------------------------------- /samples/terraform/aci/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | # --------------------------------------------------------------------------------------------------------------------- 4 | # ENVIRONMENT VARIABLES 5 | # Define these secrets as environment variables 6 | # --------------------------------------------------------------------------------------------------------------------- 7 | 8 | # ARM_CLIENT_ID 9 | # ARM_CLIENT_SECRET 10 | # ARM_SUBSCRIPTION_ID 11 | # ARM_TENANT_ID 12 | 13 | # --------------------------------------------------------------------------------------------------------------------- 14 | # REQUIRED PARAMETERS 15 | # You must provide a value for each of these parameters. 16 | # --------------------------------------------------------------------------------------------------------------------- 17 | 18 | # --------------------------------------------------------------------------------------------------------------------- 19 | # OPTIONAL PARAMETERS 20 | # These parameters have reasonable defaults. 21 | # --------------------------------------------------------------------------------------------------------------------- 22 | 23 | variable "location" { 24 | description = "The supported azure location where the resource exists" 25 | type = string 26 | } 27 | 28 | variable "postfix" { 29 | description = "A postfix string to centrally mitigate resource name collisions." 30 | type = string 31 | } -------------------------------------------------------------------------------- /samples/terraform/authorization-permission-mismatch/main.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | terraform { 5 | 6 | } 7 | 8 | provider "azurerm" { 9 | features {} 10 | skip_provider_registration = "true" 11 | storage_use_azuread = true 12 | } 13 | 14 | resource "random_id" "rg" { 15 | byte_length = 8 16 | } 17 | resource "azurerm_resource_group" "rg" { 18 | name = "rg-${random_id.rg.hex}" 19 | location = "uksouth" 20 | } 21 | 22 | resource "random_string" "rand" { 23 | length = 8 24 | special = false 25 | numeric = false 26 | upper = false 27 | lower = true 28 | } 29 | 30 | data "azurerm_client_config" "current" { 31 | } 32 | 33 | variable "location" { 34 | type = string 35 | default = "eastus" 36 | } 37 | 38 | resource "azurerm_storage_account" "st" { 39 | name = "saapermmismatch${random_string.rand.result}" 40 | resource_group_name = azurerm_resource_group.rg.name 41 | location = azurerm_resource_group.rg.location 42 | account_kind = "Storage" 43 | account_tier = "Standard" 44 | account_replication_type = "LRS" 45 | public_network_access_enabled = false 46 | } -------------------------------------------------------------------------------- /samples/terraform/existing-resource-import/main.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | resource "random_id" "rg" { 5 | byte_length = 8 6 | } 7 | resource "azurerm_resource_group" "rg" { 8 | name = "rg-${random_id.rg.hex}" 9 | location = "uksouth" 10 | } 11 | 12 | 13 | # create log analytics workspace 14 | resource "azurerm_log_analytics_workspace" "this" { 15 | location = azurerm_resource_group.rg.location 16 | name = "law${random_id.rg.hex}" 17 | resource_group_name = azurerm_resource_group.rg.name 18 | sku = "PerGB2018" 19 | retention_in_days = var.retention_in_days 20 | tags = var.tags 21 | } 22 | 23 | # resource "azurerm_application_insights" "this" { 24 | # application_type = "web" 25 | # location = azurerm_resource_group.rg.location 26 | # name = "ai-${random_id.rg.hex}" 27 | # resource_group_name = azurerm_resource_group.rg.name 28 | # workspace_id = azurerm_log_analytics_workspace.this.id 29 | # } 30 | 31 | module "application_insights" { 32 | source = "Azure/avm-res-insights-component/azurerm" 33 | # source = "github.com/Azure/terraform-azurerm-avm-res-insights-component" 34 | version = "0.1.5" 35 | resource_group_name = azurerm_resource_group.rg.name 36 | workspace_id = azurerm_log_analytics_workspace.this.id 37 | name = "ai-${random_id.rg.hex}" 38 | location = azurerm_resource_group.rg.location 39 | local_authentication_disabled = false 40 | internet_ingestion_enabled = false 41 | internet_query_enabled = false 42 | tags = var.tags 43 | enable_telemetry = true 44 | } 45 | 46 | # resource "azapi_resource" "appinsights" { 47 | # type = "Microsoft.Insights/components@2020-02-02" 48 | # name = "ai-${random_id.rg.hex}" 49 | # parent_id = azurerm_resource_group.rg.id 50 | # location = azurerm_resource_group.rg.location 51 | 52 | # body = { 53 | # kind = "web" 54 | # properties = { 55 | # Application_Type = "web" 56 | # Flow_Type = "Bluefield" 57 | # Request_Source = "rest" 58 | # IngestionMode = "LogAnalytics" 59 | # WorkspaceResourceId = azurerm_log_analytics_workspace.this.id 60 | # } 61 | # } 62 | 63 | # response_export_values = [ 64 | # "id", 65 | # "properties.ConnectionString", 66 | # ] 67 | # } 68 | 69 | -------------------------------------------------------------------------------- /samples/terraform/existing-resource-import/outputs.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | output "name" { 4 | description = "Name of the Application Insights" 5 | # value = azapi_resource.appinsights.name 6 | # value = azurerm_application_insights.this.name 7 | value = module.application_insights.name 8 | } 9 | 10 | -------------------------------------------------------------------------------- /samples/terraform/existing-resource-import/terraform.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | terraform { 4 | required_version = ">= 1.5.0" 5 | required_providers { 6 | 7 | azurerm = { 8 | source = "hashicorp/azurerm" 9 | version = "=4.9.0" 10 | } 11 | modtm = { 12 | source = "azure/modtm" 13 | version = "~> 0.3" 14 | } 15 | random = { 16 | source = "hashicorp/random" 17 | version = "~> 3.5" 18 | } 19 | azapi = { 20 | source = "Azure/azapi" 21 | version = "~> 2.0" 22 | } 23 | } 24 | } 25 | 26 | 27 | provider "azurerm" { 28 | features {} 29 | } 30 | -------------------------------------------------------------------------------- /samples/terraform/existing-resource-import/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | variable "retention_in_days" { 5 | type = number 6 | default = 90 7 | description = "(Optional) The retention period in days. 0 means unlimited." 8 | } 9 | 10 | # tflint-ignore: terraform_unused_declarations 11 | variable "tags" { 12 | type = map(string) 13 | default = null 14 | description = "(Optional) Tags of the resource." 15 | } 16 | -------------------------------------------------------------------------------- /samples/terraform/module-test-with-targetting/main.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | module "law" { 9 | source = "./modules/law" 10 | log_analytics_workspace_name = var.log_analytics_workspace_name 11 | tags = var.tags 12 | } 13 | 14 | module "law2" { 15 | source = "./modules/law" 16 | log_analytics_workspace_name = "${var.log_analytics_workspace_name}2" 17 | tags = var.tags 18 | } 19 | 20 | terraform { 21 | required_version = ">= 1.9.6, < 2.0.0" 22 | required_providers { 23 | 24 | azuread = { 25 | source = "hashicorp/azuread" 26 | version = ">= 2.53, < 3.0" 27 | } 28 | azurerm = { 29 | source = "hashicorp/azurerm" 30 | version = ">= 3.114.0, < 4.0.0" 31 | } 32 | # tflint-ignore: terraform_unused_required_providers 33 | modtm = { 34 | source = "Azure/modtm" 35 | version = "~> 0.3" 36 | } 37 | random = { 38 | source = "hashicorp/random" 39 | version = "~> 3.5" 40 | } 41 | } 42 | } 43 | 44 | provider "azurerm" { 45 | features {} 46 | skip_provider_registration = "true" 47 | } -------------------------------------------------------------------------------- /samples/terraform/module-test-with-targetting/modules/law/law.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | module "log_analytics_workspace" { 4 | source = "Azure/avm-res-operationalinsights-workspace/azurerm" 5 | version = "0.4.1" 6 | 7 | name = var.log_analytics_workspace_name 8 | location = azurerm_resource_group.this.location 9 | resource_group_name = azurerm_resource_group.this.name 10 | tags = var.tags 11 | } -------------------------------------------------------------------------------- /samples/terraform/module-test-with-targetting/modules/law/rg.tf: -------------------------------------------------------------------------------- 1 | resource "random_id" "rg" { 2 | byte_length = 8 3 | } 4 | 5 | resource "azurerm_resource_group" "this" { 6 | location = "East US2" # Location used just for the example 7 | name = "rg-${random_id.rg.hex}" 8 | } -------------------------------------------------------------------------------- /samples/terraform/module-test-with-targetting/modules/law/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "tags" { 3 | type = map(string) 4 | default = null 5 | description = "A map of tags to add to all resources" 6 | } 7 | 8 | variable "log_analytics_workspace_name" { 9 | type = string 10 | default = "" 11 | description = "The name of the Log Analytics Workspace. If not provided, a name will be generated." 12 | } 13 | 14 | -------------------------------------------------------------------------------- /samples/terraform/module-test-with-targetting/terraform.tfvars: -------------------------------------------------------------------------------- 1 | 2 | 3 | location = "eastus2" 4 | tags = { 5 | env = "test" 6 | } 7 | log_analytics_workspace_name = "lawtftest123" -------------------------------------------------------------------------------- /samples/terraform/module-test-with-targetting/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | variable "location" { 4 | type = string 5 | description = "The location/region where the resources will be deployed." 6 | nullable = false 7 | } 8 | 9 | variable "tags" { 10 | type = map(string) 11 | default = null 12 | description = "A map of tags to add to all resources" 13 | } 14 | 15 | variable "log_analytics_workspace_name" { 16 | type = string 17 | default = "" 18 | description = "The name of the Log Analytics Workspace. If not provided, a name will be generated." 19 | } 20 | 21 | -------------------------------------------------------------------------------- /samples/terraform/module-test/main.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | resource "random_id" "rg" { 5 | byte_length = 8 6 | } 7 | 8 | resource "random_integer" "rnd_num" { 9 | min = 10000 10 | max = 30000 11 | } 12 | 13 | resource "azurerm_resource_group" "this" { 14 | location = "East US2" # Location used just for the example 15 | name = "rg-${random_id.rg.hex}" 16 | } 17 | 18 | 19 | 20 | module "law" { 21 | source = "./modules/law" 22 | location = azurerm_resource_group.this.location 23 | log_analytics_workspace_name = "lawtftest${random_integer.rnd_num.result}" 24 | resource_group_name = azurerm_resource_group.this.name 25 | tags = var.tags 26 | } 27 | 28 | terraform { 29 | required_version = ">= 1.9.6, < 2.0.0" 30 | required_providers { 31 | 32 | azuread = { 33 | source = "hashicorp/azuread" 34 | version = ">= 2.53, < 3.0" 35 | } 36 | azurerm = { 37 | source = "hashicorp/azurerm" 38 | version = ">= 3.114.0, < 4.0.0" 39 | } 40 | # tflint-ignore: terraform_unused_required_providers 41 | modtm = { 42 | source = "Azure/modtm" 43 | version = "~> 0.3" 44 | } 45 | random = { 46 | source = "hashicorp/random" 47 | version = "~> 3.5" 48 | } 49 | } 50 | } 51 | 52 | provider "azurerm" { 53 | features {} 54 | skip_provider_registration = "true" 55 | } -------------------------------------------------------------------------------- /samples/terraform/module-test/modules/law/law.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | module "log_analytics_workspace" { 4 | source = "Azure/avm-res-operationalinsights-workspace/azurerm" 5 | version = "0.4.1" 6 | 7 | name = var.log_analytics_workspace_name 8 | location = var.location 9 | resource_group_name = var.resource_group_name 10 | tags = var.tags 11 | } -------------------------------------------------------------------------------- /samples/terraform/module-test/modules/law/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | variable "location" { 4 | type = string 5 | description = "The location/region where the resources will be deployed." 6 | nullable = false 7 | } 8 | 9 | # This is required for most resource modules 10 | variable "resource_group_name" { 11 | type = string 12 | description = "The resource group where the resources will be deployed." 13 | } 14 | 15 | variable "tags" { 16 | type = map(string) 17 | default = null 18 | description = "A map of tags to add to all resources" 19 | } 20 | 21 | variable "log_analytics_workspace_name" { 22 | type = string 23 | default = "" 24 | description = "The name of the Log Analytics Workspace. If not provided, a name will be generated." 25 | } 26 | 27 | -------------------------------------------------------------------------------- /samples/terraform/module-test/terraform.tfvars: -------------------------------------------------------------------------------- 1 | 2 | 3 | location = "eastus2" 4 | tags = { 5 | env = "test" 6 | } 7 | log_analytics_workspace_name = "lawtftest123" -------------------------------------------------------------------------------- /samples/terraform/module-test/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | variable "location" { 4 | type = string 5 | description = "The location/region where the resources will be deployed." 6 | nullable = false 7 | } 8 | 9 | variable "tags" { 10 | type = map(string) 11 | default = null 12 | description = "A map of tags to add to all resources" 13 | } 14 | 15 | variable "log_analytics_workspace_name" { 16 | type = string 17 | default = "" 18 | description = "The name of the Log Analytics Workspace. If not provided, a name will be generated." 19 | } 20 | 21 | -------------------------------------------------------------------------------- /samples/terraform/multi-resource/dev.vars.tfvars: -------------------------------------------------------------------------------- 1 | 2 | 3 | location = "westus" 4 | resource_group_name = "az-mpf-tf-test-rg" -------------------------------------------------------------------------------- /samples/terraform/multi-resource/main.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | terraform { 5 | 6 | } 7 | 8 | provider "azurerm" { 9 | features { 10 | resource_group { 11 | prevent_deletion_if_contains_resources = false 12 | } 13 | } 14 | skip_provider_registration = "true" 15 | } 16 | 17 | 18 | resource "azurerm_resource_group" "rg" { 19 | name = var.resource_group_name 20 | location = var.location 21 | } 22 | 23 | 24 | # Generate random postfix to mitigate naming collisions 25 | resource "random_id" "randomId" { 26 | keepers = { 27 | # Generate a new ID only when a new resource group is created 28 | resource_group = azurerm_resource_group.rg.name 29 | } 30 | 31 | byte_length = 8 32 | } 33 | 34 | # create vnet, subnet and aks in private subnet 35 | resource "azurerm_virtual_network" "vnet" { 36 | name = "vnet-${random_id.randomId.hex}" 37 | location = azurerm_resource_group.rg.location 38 | resource_group_name = azurerm_resource_group.rg.name 39 | address_space = ["10.12.0.0/16"] 40 | } 41 | 42 | resource "azurerm_subnet" "subnet" { 43 | name = "subnet-${random_id.randomId.hex}" 44 | resource_group_name = azurerm_resource_group.rg.name 45 | virtual_network_name = azurerm_virtual_network.vnet.name 46 | address_prefixes = ["10.12.1.0/24"] 47 | } 48 | 49 | # create aks cluster in subnet 50 | resource "azurerm_kubernetes_cluster" "aks" { 51 | name = "aks-${random_id.randomId.hex}" 52 | location = azurerm_resource_group.rg.location 53 | resource_group_name = azurerm_resource_group.rg.name 54 | dns_prefix = "aks-${random_id.randomId.hex}" 55 | 56 | 57 | identity { 58 | type = "SystemAssigned" 59 | } 60 | 61 | default_node_pool { 62 | name = "default" 63 | node_count = 1 64 | vm_size = "Standard_B2s" 65 | vnet_subnet_id = azurerm_subnet.subnet.id 66 | } 67 | 68 | network_profile { 69 | network_plugin = "azure" 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /samples/terraform/multi-resource/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | # --------------------------------------------------------------------------------------------------------------------- 4 | # ENVIRONMENT VARIABLES 5 | # Define these secrets as environment variables 6 | # --------------------------------------------------------------------------------------------------------------------- 7 | 8 | # ARM_CLIENT_ID 9 | # ARM_CLIENT_SECRET 10 | # ARM_SUBSCRIPTION_ID 11 | # ARM_TENANT_ID 12 | 13 | # --------------------------------------------------------------------------------------------------------------------- 14 | # REQUIRED PARAMETERS 15 | # You must provide a value for each of these parameters. 16 | # --------------------------------------------------------------------------------------------------------------------- 17 | 18 | # --------------------------------------------------------------------------------------------------------------------- 19 | # OPTIONAL PARAMETERS 20 | # These parameters have reasonable defaults. 21 | # --------------------------------------------------------------------------------------------------------------------- 22 | 23 | variable "location" { 24 | description = "The supported azure location where the resource exists" 25 | type = string 26 | default = "West US2" 27 | } 28 | 29 | variable "resource_group_name" { 30 | description = "The name of the resource group in which to create the container group." 31 | type = string 32 | default = "terratest-aci-rg-1939" 33 | } -------------------------------------------------------------------------------- /samples/terraform/rg-invalid-tf-file/main.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | # --------------------------------------------------------------------------------------------------------------------- 4 | # DEPLOY AN AZURE CONTAINER Instance 5 | # This is an example of how to deploy an Azure Container Instance 6 | # See test/terraform_azure_aci_example_test.go for how to write automated tests for this code. 7 | # --------------------------------------------------------------------------------------------------------------------- 8 | 9 | # ------------------------------------------------------------------------------ 10 | # CONFIGURE OUR AZURE CONNECTION 11 | # ------------------------------------------------------------------------------ 12 | 13 | terraform { 14 | # required_providers { 15 | # azurerm = { 16 | # version = "~>2.29.0" 17 | # source = "hashicorp/azurerm" 18 | # } 19 | # } 20 | 21 | } 22 | 23 | provider "azurerm" { 24 | features {} 25 | skip_provider_registration = "true" 26 | } 27 | 28 | # --------------------------------------------------------------------------------------------------------------------- 29 | # DEPLOY A RESOURCE GROUP 30 | # --------------------------------------------------------------------------------------------------------------------- 31 | 32 | # data "azurerm_resource_group" "rg" { 33 | # name = var.resource_group_name 34 | # } 35 | 36 | 37 | 38 | # invalid resource group name - extemely long 39 | resourc "azurerm_resource_group" "rg" { 40 | name = "rg-${random_id.rg.hex}" 41 | location = var.location 42 | } 43 | -------------------------------------------------------------------------------- /samples/terraform/rg-invalid-tf-file/output.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | output "resource_group_name" { 4 | value = azurerm_resource_group.rg.name 5 | } -------------------------------------------------------------------------------- /samples/terraform/rg-invalid-tf-file/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | # --------------------------------------------------------------------------------------------------------------------- 4 | # ENVIRONMENT VARIABLES 5 | # Define these secrets as environment variables 6 | # --------------------------------------------------------------------------------------------------------------------- 7 | 8 | # ARM_CLIENT_ID 9 | # ARM_CLIENT_SECRET 10 | # ARM_SUBSCRIPTION_ID 11 | # ARM_TENANT_ID 12 | 13 | # --------------------------------------------------------------------------------------------------------------------- 14 | # REQUIRED PARAMETERS 15 | # You must provide a value for each of these parameters. 16 | # --------------------------------------------------------------------------------------------------------------------- 17 | 18 | # --------------------------------------------------------------------------------------------------------------------- 19 | # OPTIONAL PARAMETERS 20 | # These parameters have reasonable defaults. 21 | # --------------------------------------------------------------------------------------------------------------------- 22 | 23 | variable "location" { 24 | description = "The supported azure location where the resource exists" 25 | type = string 26 | default = "West US2" 27 | } -------------------------------------------------------------------------------- /samples/terraform/rg-invalid-tfvars/dev.vars.tfvars: -------------------------------------------------------------------------------- 1 | 2 | 3 | location = 4 | -------------------------------------------------------------------------------- /samples/terraform/rg-invalid-tfvars/main.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | # --------------------------------------------------------------------------------------------------------------------- 4 | # DEPLOY AN AZURE CONTAINER Instance 5 | # This is an example of how to deploy an Azure Container Instance 6 | # See test/terraform_azure_aci_example_test.go for how to write automated tests for this code. 7 | # --------------------------------------------------------------------------------------------------------------------- 8 | 9 | # ------------------------------------------------------------------------------ 10 | # CONFIGURE OUR AZURE CONNECTION 11 | # ------------------------------------------------------------------------------ 12 | 13 | terraform { 14 | # required_providers { 15 | # azurerm = { 16 | # version = "~>2.29.0" 17 | # source = "hashicorp/azurerm" 18 | # } 19 | # } 20 | 21 | } 22 | 23 | provider "azurerm" { 24 | features {} 25 | skip_provider_registration = "true" 26 | } 27 | 28 | # --------------------------------------------------------------------------------------------------------------------- 29 | # DEPLOY A RESOURCE GROUP 30 | # --------------------------------------------------------------------------------------------------------------------- 31 | 32 | # data "azurerm_resource_group" "rg" { 33 | # name = var.resource_group_name 34 | # } 35 | 36 | # add random id to resource group name to avoid conflicts 37 | resource "random_id" "rg" { 38 | byte_length = 8 39 | } 40 | resource "azurerm_resource_group" "rg" { 41 | name = "rg-${random_id.rg.hex}" 42 | location = var.location 43 | } 44 | -------------------------------------------------------------------------------- /samples/terraform/rg-invalid-tfvars/output.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | output "resource_group_name" { 4 | value = azurerm_resource_group.rg.name 5 | } -------------------------------------------------------------------------------- /samples/terraform/rg-invalid-tfvars/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | # --------------------------------------------------------------------------------------------------------------------- 4 | # ENVIRONMENT VARIABLES 5 | # Define these secrets as environment variables 6 | # --------------------------------------------------------------------------------------------------------------------- 7 | 8 | # ARM_CLIENT_ID 9 | # ARM_CLIENT_SECRET 10 | # ARM_SUBSCRIPTION_ID 11 | # ARM_TENANT_ID 12 | 13 | # --------------------------------------------------------------------------------------------------------------------- 14 | # REQUIRED PARAMETERS 15 | # You must provide a value for each of these parameters. 16 | # --------------------------------------------------------------------------------------------------------------------- 17 | 18 | # --------------------------------------------------------------------------------------------------------------------- 19 | # OPTIONAL PARAMETERS 20 | # These parameters have reasonable defaults. 21 | # --------------------------------------------------------------------------------------------------------------------- 22 | 23 | variable "location" { 24 | description = "The supported azure location where the resource exists" 25 | type = string 26 | } -------------------------------------------------------------------------------- /samples/terraform/rg-no-tfvars/main.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | # --------------------------------------------------------------------------------------------------------------------- 4 | # DEPLOY AN AZURE CONTAINER Instance 5 | # This is an example of how to deploy an Azure Container Instance 6 | # See test/terraform_azure_aci_example_test.go for how to write automated tests for this code. 7 | # --------------------------------------------------------------------------------------------------------------------- 8 | 9 | # ------------------------------------------------------------------------------ 10 | # CONFIGURE OUR AZURE CONNECTION 11 | # ------------------------------------------------------------------------------ 12 | 13 | terraform { 14 | # required_providers { 15 | # azurerm = { 16 | # version = "~>2.29.0" 17 | # source = "hashicorp/azurerm" 18 | # } 19 | # } 20 | 21 | } 22 | 23 | provider "azurerm" { 24 | features {} 25 | skip_provider_registration = "true" 26 | } 27 | 28 | # --------------------------------------------------------------------------------------------------------------------- 29 | # DEPLOY A RESOURCE GROUP 30 | # --------------------------------------------------------------------------------------------------------------------- 31 | 32 | # data "azurerm_resource_group" "rg" { 33 | # name = var.resource_group_name 34 | # } 35 | 36 | # add random id to resource group name to avoid conflicts 37 | resource "random_id" "rg" { 38 | byte_length = 8 39 | } 40 | resource "azurerm_resource_group" "rg" { 41 | name = "rg-${random_id.rg.hex}" 42 | location = var.location 43 | } 44 | -------------------------------------------------------------------------------- /samples/terraform/rg-no-tfvars/output.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | output "resource_group_name" { 4 | value = azurerm_resource_group.rg.name 5 | } -------------------------------------------------------------------------------- /samples/terraform/rg-no-tfvars/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | # --------------------------------------------------------------------------------------------------------------------- 4 | # ENVIRONMENT VARIABLES 5 | # Define these secrets as environment variables 6 | # --------------------------------------------------------------------------------------------------------------------- 7 | 8 | # ARM_CLIENT_ID 9 | # ARM_CLIENT_SECRET 10 | # ARM_SUBSCRIPTION_ID 11 | # ARM_TENANT_ID 12 | 13 | # --------------------------------------------------------------------------------------------------------------------- 14 | # REQUIRED PARAMETERS 15 | # You must provide a value for each of these parameters. 16 | # --------------------------------------------------------------------------------------------------------------------- 17 | 18 | # --------------------------------------------------------------------------------------------------------------------- 19 | # OPTIONAL PARAMETERS 20 | # These parameters have reasonable defaults. 21 | # --------------------------------------------------------------------------------------------------------------------- 22 | 23 | variable "location" { 24 | description = "The supported azure location where the resource exists" 25 | type = string 26 | default = "West US2" 27 | } --------------------------------------------------------------------------------