Skip to content

Commit d136be4

Browse files
committed
feat(azurebackup): Add security configure-mua command for Multi-User Authorization
Add azurebackup security configure-mua command that enables/disables Multi-User Authorization (MUA) on Recovery Services vaults and Backup vaults by linking/unlinking a Resource Guard. - Enable MUA: provide --resource-guard-id to link a Resource Guard - Disable MUA: omit --resource-guard-id to unlink (protected operation) - Supports both RSV (VaultProxy) and DPP (DppResourceGuardProxy) - Auto-detects vault type when --vault-type is omitted - Proper error handling for 400/403/404/409 scenarios Files: 16 changed, ~1200 lines added Tests: 22 unit tests, 5 live tests (recorded + playback) Validation: Cspell clean, ToolDescriptionEvaluator passed
1 parent 909d6e4 commit d136be4

19 files changed

Lines changed: 913 additions & 4 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
changes:
2+
- section: "New Features"
3+
description: "Added `azurebackup security configure-mua` command to enable/disable Multi-User Authorization (MUA) on Recovery Services vaults and Backup vaults by linking/unlinking a Resource Guard."

‎servers/Azure.Mcp.Server/docs/azmcp-commands.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,19 @@ azmcp azurebackup disasterrecovery enable-crr --subscription <subscription> \
949949
[--vault-type <vault-type>]
950950
```
951951
952+
#### Security
953+
954+
```bash
955+
# Configures Multi-User Authorization (MUA) on a vault by linking or unlinking a Resource Guard.
956+
# Provide --resource-guard-id to enable MUA. Omit to disable MUA (protected operation).
957+
# ✅ Destructive | ✅ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
958+
azmcp azurebackup security configure-mua --subscription <subscription> \
959+
--resource-group <resource-group> \
960+
--vault <vault> \
961+
[--vault-type <vault-type>] \
962+
[--resource-guard-id <resource-guard-id>]
963+
```
964+
952965
### Azure CLI Operations
953966
954967
#### Generate

‎servers/Azure.Mcp.Server/docs/e2eTestPrompts.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ This file contains prompts used for end-to-end testing to ensure each tool is in
158158
| azurebackup_protecteditem_undelete | Undelete the accidentally deleted backup for VM <datasource_id> in vault <vault_name> under resource group <resource_group> |
159159
| azurebackup_recoverypoint_get | Get recovery points for protected item <item_name> in vault <vault_name> and resource group <resource_group> |
160160
| azurebackup_recoverypoint_get | List available recovery points for <item_name> in vault <vault_name> under resource group <resource_group> |
161+
| azurebackup_security_configure-mua | Enable multi-user authorization on vault <vault_name> in resource group <resource_group> with resource guard <resource_guard_id> |
162+
| azurebackup_security_configure-mua | Disable MUA on vault <vault_name> in resource group <resource_group> |
161163
| azurebackup_vault_create | Create a Recovery Services vault named <vault_name> in resource group <resource_group> in region <location> with vault-type 'rsv' |
162164
| azurebackup_vault_create | Set up a new backup vault called <vault_name> in <location> under resource group <resource_group> with vault-type 'dpp' |
163165
| azurebackup_vault_get | Get details of Recovery Services vault <vault_name> in resource group <resource_group> |

‎servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3805,7 +3805,8 @@
38053805
"azurebackup_governance_immutability",
38063806
"azurebackup_governance_soft-delete",
38073807
"azurebackup_disasterrecovery_enable-crr",
3808-
"azurebackup_protecteditem_undelete"
3808+
"azurebackup_protecteditem_undelete",
3809+
"azurebackup_security_configure-mua"
38093810
]
38103811
},
38113812
{

‎tools/Azure.Mcp.Tools.AzureBackup/src/AzureBackupSetup.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Azure.Mcp.Tools.AzureBackup.Commands.ProtectableItem;
1010
using Azure.Mcp.Tools.AzureBackup.Commands.ProtectedItem;
1111
using Azure.Mcp.Tools.AzureBackup.Commands.RecoveryPoint;
12+
using Azure.Mcp.Tools.AzureBackup.Commands.Security;
1213
using Azure.Mcp.Tools.AzureBackup.Commands.Vault;
1314
using Azure.Mcp.Tools.AzureBackup.Services;
1415
using Microsoft.Extensions.DependencyInjection;
@@ -53,6 +54,8 @@ public void ConfigureServices(IServiceCollection services)
5354
services.AddSingleton<GovernanceSoftDeleteCommand>();
5455

5556
services.AddSingleton<DisasterRecoveryEnableCrrCommand>();
57+
58+
services.AddSingleton<SecurityConfigureMuaCommand>();
5659
}
5760

5861
public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
@@ -109,6 +112,10 @@ and Backup vaults (DPP/Data Protection). Supports vault management, protected it
109112
azureBackup.AddSubGroup(disasterrecovery);
110113
disasterrecovery.AddCommand<DisasterRecoveryEnableCrrCommand>(serviceProvider);
111114

115+
var security = new CommandGroup("security", "Security operations - Configure Multi-User Authorization (MUA) for backup vaults.");
116+
azureBackup.AddSubGroup(security);
117+
security.AddCommand<SecurityConfigureMuaCommand>(serviceProvider);
118+
112119
return azureBackup;
113120
}
114121
}

‎tools/Azure.Mcp.Tools.AzureBackup/src/Commands/AzureBackupJsonContext.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Azure.Mcp.Tools.AzureBackup.Commands.ProtectableItem;
1212
using Azure.Mcp.Tools.AzureBackup.Commands.ProtectedItem;
1313
using Azure.Mcp.Tools.AzureBackup.Commands.RecoveryPoint;
14+
using Azure.Mcp.Tools.AzureBackup.Commands.Security;
1415
using Azure.Mcp.Tools.AzureBackup.Commands.Vault;
1516
using Azure.Mcp.Tools.AzureBackup.Models;
1617

@@ -32,6 +33,7 @@ namespace Azure.Mcp.Tools.AzureBackup.Commands;
3233
[JsonSerializable(typeof(GovernanceImmutabilityCommand.GovernanceImmutabilityCommandResult))]
3334
[JsonSerializable(typeof(GovernanceSoftDeleteCommand.GovernanceSoftDeleteCommandResult))]
3435
[JsonSerializable(typeof(DisasterRecoveryEnableCrrCommand.DisasterRecoveryEnableCrrCommandResult))]
36+
[JsonSerializable(typeof(SecurityConfigureMuaCommand.SecurityConfigureMuaCommandResult))]
3537
[JsonSerializable(typeof(BackupVaultInfo))]
3638
[JsonSerializable(typeof(ProtectedItemInfo))]
3739
[JsonSerializable(typeof(BackupPolicyInfo))]
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Net;
5+
using Azure.Mcp.Tools.AzureBackup.Models;
6+
using Azure.Mcp.Tools.AzureBackup.Options;
7+
using Azure.Mcp.Tools.AzureBackup.Options.Security;
8+
using Azure.Mcp.Tools.AzureBackup.Services;
9+
using Microsoft.Extensions.Logging;
10+
using Microsoft.Mcp.Core.Commands;
11+
using Microsoft.Mcp.Core.Extensions;
12+
using Microsoft.Mcp.Core.Models.Command;
13+
14+
namespace Azure.Mcp.Tools.AzureBackup.Commands.Security;
15+
16+
[CommandMetadata(
17+
Id = "c3a21f68-9b5e-4d1a-bf3c-7e2a0f8d4b19",
18+
Name = "configure-mua",
19+
Title = "Configure Multi-User Authorization",
20+
Description = """
21+
Configures Multi-User Authorization (MUA) on a vault by linking or unlinking a Resource Guard.
22+
Provide --resource-guard-id to enable MUA, protecting critical operations (disable soft delete,
23+
remove immutability, stop protection) so they require approval from a security admin with
24+
permissions on the Resource Guard. Omit --resource-guard-id to disable MUA (this itself is a
25+
protected operation requiring Backup MUA Operator role on the Resource Guard).
26+
""",
27+
Destructive = true,
28+
Idempotent = true,
29+
OpenWorld = false,
30+
ReadOnly = false,
31+
Secret = false,
32+
LocalRequired = false)]
33+
public sealed class SecurityConfigureMuaCommand(ILogger<SecurityConfigureMuaCommand> logger, IAzureBackupService azureBackupService) : BaseAzureBackupCommand<SecurityConfigureMuaOptions>()
34+
{
35+
private readonly ILogger<SecurityConfigureMuaCommand> _logger = logger;
36+
private readonly IAzureBackupService _azureBackupService = azureBackupService;
37+
38+
protected override void RegisterOptions(Command command)
39+
{
40+
base.RegisterOptions(command);
41+
command.Options.Add(AzureBackupOptionDefinitions.ResourceGuardId);
42+
command.Validators.Add(commandResult =>
43+
{
44+
if (commandResult.HasOptionResult(AzureBackupOptionDefinitions.ResourceGuardId.Name))
45+
{
46+
var value = commandResult.GetValue<string>(AzureBackupOptionDefinitions.ResourceGuardId.Name);
47+
if (!string.IsNullOrEmpty(value) &&
48+
!value.StartsWith("/subscriptions/", StringComparison.OrdinalIgnoreCase))
49+
{
50+
commandResult.AddError("--resource-guard-id must be a valid ARM resource ID starting with '/subscriptions/'.");
51+
}
52+
}
53+
});
54+
}
55+
56+
protected override SecurityConfigureMuaOptions BindOptions(ParseResult parseResult)
57+
{
58+
var options = base.BindOptions(parseResult);
59+
options.ResourceGuardId = parseResult.GetValueOrDefault<string>(AzureBackupOptionDefinitions.ResourceGuardId.Name);
60+
return options;
61+
}
62+
63+
public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken)
64+
{
65+
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
66+
{
67+
return context.Response;
68+
}
69+
70+
var options = BindOptions(parseResult);
71+
72+
AzureBackupTelemetryTags.AddVaultTags(context.Activity, options.VaultType);
73+
74+
try
75+
{
76+
OperationResult result;
77+
78+
if (!string.IsNullOrEmpty(options.ResourceGuardId))
79+
{
80+
result = await _azureBackupService.ConfigureMultiUserAuthorizationAsync(
81+
options.Vault!,
82+
options.ResourceGroup!,
83+
options.Subscription!,
84+
options.ResourceGuardId,
85+
options.VaultType,
86+
options.Tenant,
87+
options.RetryPolicy,
88+
cancellationToken);
89+
}
90+
else
91+
{
92+
result = await _azureBackupService.DisableMultiUserAuthorizationAsync(
93+
options.Vault!,
94+
options.ResourceGroup!,
95+
options.Subscription!,
96+
options.VaultType,
97+
options.Tenant,
98+
options.RetryPolicy,
99+
cancellationToken);
100+
}
101+
102+
context.Response.Results = ResponseResult.Create(
103+
new(result),
104+
AzureBackupJsonContext.Default.SecurityConfigureMuaCommandResult);
105+
}
106+
catch (Exception ex)
107+
{
108+
_logger.LogError(ex, "Error configuring MUA. Vault: {Vault}, ResourceGuardId: {ResourceGuardId}",
109+
options.Vault, options.ResourceGuardId);
110+
HandleException(context, ex);
111+
}
112+
113+
return context.Response;
114+
}
115+
116+
protected override string GetErrorMessage(Exception ex) => ex switch
117+
{
118+
ArgumentException argEx => argEx.Message,
119+
UnauthorizedAccessException => "Authorization failed. Verify your RBAC permissions on the vault, or specify --vault-type to skip auto-detection.",
120+
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound =>
121+
"Vault or Resource Guard not found, or MUA is not enabled for this vault. Verify the vault name, resource group, and Resource Guard ID. If you are disabling MUA, ensure MUA is currently configured.",
122+
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.BadRequest =>
123+
$"Bad request configuring MUA. Ensure the Resource Guard is in the same region as the vault. Details: {reqEx.Message}",
124+
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden =>
125+
$"Authorization failed. To enable MUA, you need Reader role on the Resource Guard. To disable MUA, you need Backup MUA Operator role on the Resource Guard. Details: {reqEx.Message}",
126+
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Conflict =>
127+
"MUA configuration conflict. The vault may already have a Resource Guard linked, or the operation is blocked by the current MUA configuration.",
128+
RequestFailedException reqEx => reqEx.Message,
129+
_ => base.GetErrorMessage(ex)
130+
};
131+
132+
protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch
133+
{
134+
UnauthorizedAccessException => HttpStatusCode.Forbidden,
135+
ArgumentException or FormatException => HttpStatusCode.BadRequest,
136+
RequestFailedException reqEx => (HttpStatusCode)reqEx.Status,
137+
_ => base.GetStatusCode(ex)
138+
};
139+
140+
internal record SecurityConfigureMuaCommandResult(OperationResult Result);
141+
}

‎tools/Azure.Mcp.Tools.AzureBackup/src/Options/AzureBackupOptionDefinitions.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public static class AzureBackupOptionDefinitions
2929
public const string VmResourceIdName = "vm-resource-id";
3030
public const string ResourceTypeFilterName = "resource-type-filter";
3131
public const string TagFilterName = "tag-filter";
32+
public const string ResourceGuardIdName = "resource-guard-id";
3233

3334
public static readonly Option<string> Vault = new($"--{VaultName}")
3435
{
@@ -173,4 +174,10 @@ public static class AzureBackupOptionDefinitions
173174
Description = "Tag-based filter in key=value format (e.g., 'environment=production').",
174175
Required = false
175176
};
177+
178+
public static readonly Option<string> ResourceGuardId = new($"--{ResourceGuardIdName}")
179+
{
180+
Description = "ARM resource ID of the Resource Guard to link for Multi-User Authorization (e.g., '/subscriptions/.../resourceGroups/.../providers/Microsoft.DataProtection/resourceGuards/myGuard').",
181+
Required = false
182+
};
176183
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Azure.Mcp.Tools.AzureBackup.Options.Security;
7+
8+
public class SecurityConfigureMuaOptions : BaseAzureBackupOptions
9+
{
10+
[JsonPropertyName(AzureBackupOptionDefinitions.ResourceGuardIdName)]
11+
public string? ResourceGuardId { get; set; }
12+
}

‎tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,29 @@ public async Task<OperationResult> ConfigureCrossRegionRestoreAsync(
645645
return await dppOps.ConfigureCrossRegionRestoreAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken);
646646
}
647647

648+
public async Task<OperationResult> ConfigureMultiUserAuthorizationAsync(
649+
string vaultName, string resourceGroup, string subscription,
650+
string resourceGuardId, string? vaultType, string? tenant,
651+
RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken)
652+
{
653+
var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken);
654+
return VaultTypeResolver.IsRsv(resolved)
655+
? await rsvOps.ConfigureMultiUserAuthorizationAsync(vaultName, resourceGroup, subscription, resourceGuardId, tenant, retryPolicy, cancellationToken)
656+
: await dppOps.ConfigureMultiUserAuthorizationAsync(vaultName, resourceGroup, subscription, resourceGuardId, tenant, retryPolicy, cancellationToken);
657+
}
658+
659+
public async Task<OperationResult> DisableMultiUserAuthorizationAsync(
660+
string vaultName, string resourceGroup, string subscription,
661+
string? vaultType, string? tenant,
662+
RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken)
663+
{
664+
var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken);
665+
return VaultTypeResolver.IsRsv(resolved)
666+
? await rsvOps.DisableMultiUserAuthorizationAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken)
667+
: await dppOps.DisableMultiUserAuthorizationAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken);
668+
}
669+
670+
648671
private async Task<string> ResolveVaultTypeAsync(
649672
string vaultName, string resourceGroup, string subscription,
650673
string? vaultType, string? tenant,

0 commit comments

Comments
 (0)