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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/armCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ func getMPFARM(cmd *cobra.Command, args []string) {
}

displayResult(mpfResult, displayOptions)

suggestAndDisplayRoles(ctx, mpfConfig.SubscriptionID, mpfResult)
}

func getDislayOptions(flgShowDetailedOutput bool, flgJSONOutput bool, subscriptionID string) presentation.DisplayOptions {
Expand Down
1 change: 1 addition & 0 deletions cmd/bicepCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,5 @@ func getMPFBicep(cmd *cobra.Command, args []string) {

displayResult(mpfResult, displayOptions)

suggestAndDisplayRoles(ctx, mpfConfig.SubscriptionID, mpfResult)
}
82 changes: 82 additions & 0 deletions cmd/roleSuggestion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// MIT License
//
// Copyright (c) Microsoft Corporation.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE

package main

import (
"context"
"os"

"github.com/Azure/mpf/pkg/domain"
roledefinitionmanager "github.com/Azure/mpf/pkg/infrastructure/roleDefinitionManager"
"github.com/Azure/mpf/pkg/presentation"

log "github.com/sirupsen/logrus"
)

// suggestAndDisplayRoles fetches the Azure built-in role definitions, matches them
// against the required permissions discovered by MPF, and displays the suggested
// role(s). It is a no-op unless the --suggestRoles flag is set. Failures are
// logged but do not abort the command, since the primary permissions result has
// already been displayed.
func suggestAndDisplayRoles(ctx context.Context, subscriptionID string, mpfResult domain.MPFResult) {
if !flgSuggestRoles {
return
}

requiredPermissions := flattenRequiredPermissions(mpfResult.RequiredPermissions)
if len(requiredPermissions) == 0 {
log.Warnln("No permissions available to suggest built-in roles for")
return
}

log.Infoln("Fetching Azure built-in role definitions to suggest matching roles...")
roleProvider := roledefinitionmanager.NewRoleDefinitionManager(subscriptionID)
builtInRoles, err := roleProvider.GetBuiltInRoles(ctx, subscriptionID)
if err != nil {
log.Errorf("Error fetching built-in roles for role suggestion: %v", err)
return
}

suggestion := domain.SuggestBuiltInRoles(requiredPermissions, builtInRoles)

if err := presentation.DisplayRoleSuggestion(os.Stdout, suggestion, flgJSONOutput); err != nil {
log.Errorf("Error displaying role suggestion: %v", err)
}
Comment on lines +60 to +64
}

// flattenRequiredPermissions collects the unique permissions across all scopes in
// the MPF result, since a role assignment grants the union of these permissions.
func flattenRequiredPermissions(requiredPermissions map[string][]string) []string {
seen := make(map[string]bool)
var all []string
for _, perms := range requiredPermissions {
for _, perm := range perms {
if perm == "" || seen[perm] {
continue
}
seen[perm] = true
all = append(all, perm)
}
}
return all
}
2 changes: 2 additions & 0 deletions cmd/rootCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ var (
flgVerbose bool
flgDebug bool
flgInitialPermissions string
flgSuggestRoles bool
// RootCmd *cobra.Command
)

Expand Down Expand Up @@ -88,6 +89,7 @@ func NewRootCommand() *cobra.Command {
rootCmd.PersistentFlags().BoolVarP(&flgVerbose, "verbose", "v", false, "verbose output")
rootCmd.PersistentFlags().BoolVarP(&flgDebug, "debug", "d", false, "debug output")
rootCmd.PersistentFlags().StringVarP(&flgInitialPermissions, "initialPermissions", "", "", "Initial permissions to add to the custom role before starting MPF analysis. Can be a comma-separated list (e.g., 'perm1,perm2') or @path/to/file.json to load from a JSON file with format: {\"RequiredPermissions\":{\"\":[\"perm1\",\"perm2\"]}}.")
rootCmd.PersistentFlags().BoolVarP(&flgSuggestRoles, "suggestRoles", "", false, "After computing the minimum permissions, suggest Azure built-in role(s) that cover them")

err := rootCmd.MarkPersistentFlagRequired("subscriptionID")
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cmd/terraformCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,5 @@ func getMPFTerraform(cmd *cobra.Command, args []string) {

displayResult(mpfResult, displayOptions)

suggestAndDisplayRoles(ctx, mpfConfig.SubscriptionID, mpfResult)
}
49 changes: 37 additions & 12 deletions docs/commandline-flags-and-env-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@

## Global Flags (Common to all providers)

| Flag | Environment Variable | Required / Optional | Description |
|--------------------|------------------------|---------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| subscriptionID | MPF_SUBSCRIPTIONID | Required | |
| tenantID | MPF_TENANTID | Required | |
| spClientID | MPF_SPCLIENTID | Required | |
| spObjectID | MPF_SPOBJECTID | Required | Note this is the SP Object id and is different from the Client ID |
| spClientSecret | MPF_SPCLIENTSECRET | Required | |
| 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 |
| jsonOutput | MPF_JSONOUTPUT | Optional | If set to true, the detailed output is printed in JSON format |
| verbose | MPF_VERBOSE | Optional | If set to true, verbose output with informational messages is displayed |
| debug | MPF_DEBUG | Optional | If set to true, output with detailed debug messages is displayed. The debug messages may contain sensitive tokens |
| initialPermissions | MPF_INITIALPERMISSIONS | Optional | Initial permissions to seed the custom role with before MPF analysis. See [Initial Permissions](#initial-permissions) for details |
| Flag | Environment Variable | Required / Optional | Description |
|--------------------|------------------------|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| subscriptionID | MPF_SUBSCRIPTIONID | Required | |
| tenantID | MPF_TENANTID | Required | |
| spClientID | MPF_SPCLIENTID | Required | |
| spObjectID | MPF_SPOBJECTID | Required | Note this is the SP Object id and is different from the Client ID |
| spClientSecret | MPF_SPCLIENTSECRET | Required | |
| 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 |
| jsonOutput | MPF_JSONOUTPUT | Optional | If set to true, the detailed output is printed in JSON format |
| verbose | MPF_VERBOSE | Optional | If set to true, verbose output with informational messages is displayed |
| debug | MPF_DEBUG | Optional | If set to true, output with detailed debug messages is displayed. The debug messages may contain sensitive tokens |
| initialPermissions | MPF_INITIALPERMISSIONS | Optional | Initial permissions to seed the custom role with before MPF analysis. See [Initial Permissions](#initial-permissions) for details |
| suggestRoles | MPF_SUGGESTROLES | Optional | If set to true, after computing the minimum permissions MPF suggests Azure built-in role(s) that cover them. See [Suggest Built-In Roles](#suggest-built-in-roles) for details |

When used for Terraform, the verbose and debug flags show detailed logs from Terraform.

Expand Down Expand Up @@ -88,6 +89,30 @@ terraform init

The `--targetModule` value follows Terraform's module address syntax (e.g., `module.law`). You can combine this with other flags like `--jsonOutput` or `--initialPermissions`.

## Suggest Built-In Roles

The `--suggestRoles` flag makes MPF, after it has computed the minimum permissions, query the Azure built-in role definitions and suggest which built-in role(s) cover those permissions. This helps when you would rather assign an existing built-in role than create a custom role.

The suggestion output contains three parts:

- **Single-role matches**: built-in roles that each cover every required permission on their own. They are ordered from most specific (least privilege) to broadest, so narrowly scoped roles appear first and broad roles such as `Contributor` and `Owner` appear last.
- **Minimal combination**: a small set of built-in roles that together cover the required permissions, chosen with a greedy least-privilege heuristic. This is useful when no single built-in role covers everything.
- **Uncovered permissions**: any required permissions that no built-in role grants. When present, a custom role is still required for those.

### Usage

```bash
azmpf arm \
--templateFilePath ./template.json \
--parametersFilePath ./parameters.json \
--suggestRoles \
# ... other flags
```

Use `--suggestRoles --jsonOutput` to receive the suggestion as JSON for further processing.

> Note: The suggestion is based on the control-plane actions discovered by MPF and Azure's built-in role definitions at the time the command runs. Always review the suggested role's full permission set before assigning it.

## Initial Permissions

The `--initialPermissions` flag allows you to specify permissions that should be added to the custom role before MPF starts its analysis. This is particularly useful when:
Expand Down
Loading
Loading