Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b113c05
Initial commit for updating MSI permissions
SamarthMayya May 24, 2022
4a02b49
Merge branch 'main' of https://github.com/Azure/azure-cli-extensions …
SamarthMayya May 24, 2022
fb98d3d
Added permission scope for disk
SamarthMayya May 24, 2022
1c67cac
Added code style changes
SamarthMayya May 24, 2022
c51c65d
Merge branch 'main' of https://github.com/Azure/azure-cli-extensions …
SamarthMayya May 25, 2022
34fa75e
Check role assignments at lowest possible scope
SamarthMayya May 25, 2022
088c630
Merge branch 'main' of https://github.com/Azure/azure-cli-extensions …
SamarthMayya May 27, 2022
216326b
Initial commit for Blob permissions assignment and argument validation
SamarthMayya May 27, 2022
7b14dc2
Added code style changes
SamarthMayya May 27, 2022
1679978
Made changes for better UX
SamarthMayya May 27, 2022
943003e
Added help text for command
SamarthMayya May 27, 2022
df07ad2
Merge branch 'main' of https://github.com/Azure/azure-cli-extensions …
SamarthMayya May 27, 2022
5ad7426
Resolve merge conflict
SamarthMayya May 30, 2022
3c953a6
Covered positive cases for updating permissions
SamarthMayya Jun 10, 2022
98d6c0a
Used Manifest based approach for generalizing code
SamarthMayya Jun 11, 2022
d530665
Merge branch 'main' of https://github.com/Azure/azure-cli-extensions …
SamarthMayya Jun 11, 2022
cac88ed
Merge branch 'main' of https://github.com/Azure/azure-cli-extensions …
SamarthMayya Jun 13, 2022
c78db66
Organized code with helper functions
SamarthMayya Jun 14, 2022
c17efa9
Added style changes for pipeline
SamarthMayya Jun 14, 2022
b93171d
Merge branch 'main' of https://github.com/Azure/azure-cli-extensions …
SamarthMayya Jun 14, 2022
21b443c
Added test for AzureDisk
SamarthMayya Jun 14, 2022
86dd156
Deleted existing recording
SamarthMayya Jun 14, 2022
effc1ea
Recording for Disk test created
SamarthMayya Jun 15, 2022
a013554
Added passing test for AzureDisk
SamarthMayya Jun 15, 2022
e7fd756
Added working test for AzureBlob
SamarthMayya Jun 15, 2022
3a66527
Updated API version and corresponding tests
SamarthMayya Jun 15, 2022
2700b97
Added snapshot rg param to initialize command, and validation to ensu…
SamarthMayya Jun 16, 2022
d95b039
Added --snapshot-rg param in test
SamarthMayya Jun 16, 2022
b7baee2
Fix style errors
SamarthMayya Jun 16, 2022
c8564f1
Added validation to ensure secret URI is proper, and added network ac…
SamarthMayya Jun 17, 2022
2305a3c
Merge branch 'main' of https://github.com/Azure/azure-cli-extensions …
SamarthMayya Jun 17, 2022
84e5c63
Added test for PostgreSQL workload
SamarthMayya Jun 21, 2022
3a82eda
Added code style changes
SamarthMayya Jun 21, 2022
b9439b7
Changed version and refactored code to use inbuilt validation for ARM ID
SamarthMayya Jun 22, 2022
8a695eb
Added a wait time of 60 seconds to let the roles propagate
SamarthMayya Jun 22, 2022
4c14365
Minor code restructuring, and modify firewall client context to have …
SamarthMayya Jun 28, 2022
4979d27
Style Change added
SamarthMayya Jun 28, 2022
b59065f
Changed general help text
SamarthMayya Jun 29, 2022
d7508d5
Added logging for 60 seconds wait time
SamarthMayya Jun 29, 2022
270cd3d
Changed help text and output of command
SamarthMayya Jun 29, 2022
10f72f4
Updated version in version.py and added output customization for keyv…
SamarthMayya Jun 30, 2022
39f172e
Merge branch 'main' of https://github.com/Azure/azure-cli-extensions …
SamarthMayya Jun 30, 2022
ddb4e4d
Added validation to ensure that backup instance is of same datasource…
SamarthMayya Jun 30, 2022
dc022a7
Changed error message
SamarthMayya Jun 30, 2022
1dbf8fb
Moved permission object formation into helper code
SamarthMayya Jun 30, 2022
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
4 changes: 4 additions & 0 deletions src/dataprotection/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Release History
===============
0.5.0
++++++
* `az dataprotection backup-instance update-msi-permissions`: New command to grant missing permissions to backup vault MSI
* `az dataprotection backup-instance initialize`: Added optional `--snapshot-resource-group-name` parameter

0.4.0
++++++
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
"allowedRestoreTargetTypes": [ "OriginalLocation" ],
"itemLevelRecoveyEnabled": true,
"supportSecretStoreAuthentication": false,
"backupVaultPermissions": [
{
"roleDefinitionName": "Storage Account Backup Contributor",
"type": "DataSource"
}
],
"policySettings": {
"supportedRetentionTags": [],
"supportedDatastoreTypes": [ "OperationalStore" ],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@
"allowedRestoreTargetTypes": [ "AlternateLocation", "RestoreAsFiles" ],
"itemLevelRecoveyEnabled": false,
"supportSecretStoreAuthentication": true,
"backupVaultPermissions": [
{
"roleDefinitionName": "Reader",
"type": "DataSource"
}
],
"secretStorePermissions": {
"rbacModel": {
"roleDefinitionName": "Key Vault Secrets User"
},
"vaultAccessPolicyModel": {
"accessPolicies": {
"permissions": {
"secrets": [ "Get", "List" ]
}
}
}
},
"policySettings": {
"supportedRetentionTags": [ "Weekly", "Monthly", "Yearly" ],
"supportedDatastoreTypes": [ "VaultStore", "ArchiveStore" ],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@
"allowedRestoreTargetTypes": [ "AlternateLocation" ],
"itemLevelRecoveyEnabled": false,
"supportSecretStoreAuthentication": false,
"backupVaultPermissions": [
{
"roleDefinitionName": "Disk Backup Reader",
"type": "DataSource"
},
{
"roleDefinitionName": "Disk Snapshot Contributor",
"type": "SnapshotRG"
}
],
"policySettings": {
"supportedRetentionTags": [ "Daily", "Weekly" ],
"supportedDatastoreTypes": [ "OperationalStore" ],
Expand Down
8 changes: 8 additions & 0 deletions src/dataprotection/azext_dataprotection/manual/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@
text: az dataprotection backup-instance list-from-resourcegraph --resource-groups resourceGroup --vaults vault --protection-status ProtectionError --datasource-type AzureDisk
"""

helps['dataprotection backup-instance update-msi-permissions'] = """
type: command
short-summary: Assign the required permissions needed to successfully enable backup for the datasource.
examples:
- name: Assign the required permissions needed to successfully enable backup for the datasource.
text: az dataprotection backup-instance update-msi-permissions --backup-instance backup_inst.json --resource-group samarth_resource_group --vault-name samarthbackupvault --datasource-type AzureDisk --operation Backup --permissions-scope ResourceGroup
"""

helps['dataprotection backup-policy get-default-policy-template'] = """
type: command
short-summary: Get default policy template for a given datasource type.
Expand Down
13 changes: 13 additions & 0 deletions src/dataprotection/azext_dataprotection/manual/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
get_datasource_types,
get_rehydration_priority_values,
get_secret_store_type_values,
get_backup_operation_values,
get_permission_scope_values,
get_resource_type_values,
get_critical_operation_values
)
Expand All @@ -56,6 +58,7 @@ def load_arguments(self, _):
c.argument('policy_id', type=str, help="Id of the backup policy the datasource will be associated")
c.argument('secret_store_type', arg_type=get_enum_type(get_secret_store_type_values()), help="Specify the secret store type to use for authentication")
c.argument('secret_store_uri', type=str, help="specify the secret store uri to use for authentication")
c.argument('snapshot_resource_group_name', options_list=['--snapshot-resource-group-name', '--snapshot-rg'], type=str, help="Name of the resource group in which the backup snapshots should be stored")

with self.argument_context('dataprotection backup-instance update-policy') as c:
c.argument('backup_instance_name', type=str, help="Backup instance name.")
Expand Down Expand Up @@ -83,6 +86,16 @@ def load_arguments(self, _):
c.argument('protection_status', arg_type=get_enum_type(get_protection_status_values()), nargs='+', help="specify protection status.")
c.argument('datasource_id', type=str, nargs='+', help="specify datasource id filter to apply.")

with self.argument_context('dataprotection backup-instance update-msi-permissions') as c:
c.argument('operation', arg_type=get_enum_type(get_backup_operation_values()), help="List of possible operations")
c.argument('datasource_type', arg_type=get_enum_type(get_datasource_types()), help="Specify the datasource type of the resource to be backed up")
c.argument('vault_name', type=str, help="Name of the vault.")
c.argument('permissions_scope', arg_type=get_enum_type(get_permission_scope_values()), help="Scope for assigning permissions to the backup vault")
c.argument('keyvault_id', type=str, help='ARM id of the key vault. Required when --datasource-type is AzureDatabaseForPostgreSQL')
c.argument('yes', options_list=['--yes', '-y'], help='Do not prompt for confirmation.', action='store_true')
c.argument('backup_instance', type=validate_file_or_dict, help='Request body for operation Expected value: '
'json-string/@json-file. Required when --operation is Backup')

with self.argument_context('dataprotection job list-from-resourcegraph') as c:
c.argument('subscriptions', type=str, nargs='+', help="List of subscription Ids.")
c.argument('resource_groups', type=str, nargs='+', help="List of resource groups.")
Expand Down
1 change: 1 addition & 0 deletions src/dataprotection/azext_dataprotection/manual/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def load_command_table(self, _):
g.custom_command('restore trigger', 'dataprotection_backup_instance_restore_trigger', supports_no_wait=True)
g.custom_command('update-policy', "dataprotection_backup_instance_update_policy", supports_no_wait=True)
g.custom_command('list-from-resourcegraph', 'dataprotection_backup_instance_list_from_resourcegraph', client_factory=cf_resource_graph_client)
g.custom_command('update-msi-permissions', 'dataprotection_backup_instance_update_msi_permissions', client_factory=cf_backup_vault)

with self.command_group('dataprotection backup-policy', exception_handler=exception_handler) as g:
g.custom_command('get-default-policy-template', "dataprotection_backup_policy_get_default_policy_template")
Expand Down
190 changes: 188 additions & 2 deletions src/dataprotection/azext_dataprotection/manual/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import uuid
import copy
import re
import time
from knack.util import CLIError
from knack.log import get_logger
from azure.cli.core.util import sdk_no_wait
Expand Down Expand Up @@ -170,7 +171,8 @@ def dataprotection_backup_instance_validate_for_backup(client, vault_name, resou


def dataprotection_backup_instance_initialize(datasource_type, datasource_id, datasource_location, policy_id,
secret_store_type=None, secret_store_uri=None):
secret_store_type=None, secret_store_uri=None,
snapshot_resource_group_name=None):
datasource_info = helper.get_datasource_info(datasource_type, datasource_id, datasource_location)
datasourceset_info = None
manifest = helper.load_manifest(datasource_type)
Expand All @@ -185,11 +187,16 @@ def dataprotection_backup_instance_initialize(datasource_type, datasource_id, da
{
"object_type": "AzureOperationalStoreParameters",
"data_store_type": "OperationalStore",
"resource_group_id": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}"
"resource_group_id": helper.get_rg_id_from_arm_id(datasource_id)
}
]
}

if snapshot_resource_group_name:
disk_sub_id = helper.get_sub_id_from_arm_id(datasource_id)
policy_parameters["data_store_parameters_list"][0]["resource_group_id"] = (disk_sub_id + "/resourceGroups/"
+ snapshot_resource_group_name)

datasource_auth_credentials_info = None
if manifest["supportSecretStoreAuthentication"]:
if secret_store_uri and secret_store_type:
Expand Down Expand Up @@ -255,6 +262,185 @@ def dataprotection_backup_instance_list_from_resourcegraph(client, datasource_ty
return response.data


def dataprotection_backup_instance_update_msi_permissions(cmd, client, resource_group_name, datasource_type, vault_name, operation, permissions_scope, backup_instance=None, keyvault_id=None, yes=False):
from msrestazure.tools import is_valid_resource_id, parse_resource_id

if operation == 'Backup' and backup_instance is None:
raise CLIError("--backup-instance needs to be given when --operation is given as Backup")

if datasource_type == 'AzureDatabaseForPostgreSQL':
if not keyvault_id:
raise CLIError("--keyvault-id needs to be given when --datasource-type is AzureDatabaseForPostgreSQL")

if not is_valid_resource_id(keyvault_id):
raise CLIError("Please provide a valid keyvault ID")

datasource_map = {
"AzureDisk": "Microsoft.Compute/disks",
"AzureBlob": "Microsoft.Storage/storageAccounts/blobServices",
"AzureDatabaseForPostgreSQL": "Microsoft.DBforPostgreSQL/servers/databases"
}

if datasource_map[datasource_type] != backup_instance["properties"]["data_source_info"]["datasource_type"]:
raise CLIError("--backup-instance provided is not compatible with the --datasource-type.")

from azure.cli.core.commands.client_factory import get_mgmt_service_client

from knack.prompting import prompt_y_n
msg = helper.get_help_text_on_grant_permissions(datasource_type)
if not yes and not prompt_y_n(msg):
return None

backup_vault = client.get(resource_group_name=resource_group_name,
vault_name=vault_name)
principal_id = backup_vault.identity.principal_id

role_assignments_arr = []

if backup_instance['properties']['data_source_info']['resource_location'] != backup_vault.location:
raise CLIError("Location of data source needs to be the same as backup vault.\nMake sure the datasource "
"and vault are chosen properly")

from azure.cli.command_modules.role.custom import list_role_assignments, create_role_assignment

manifest = helper.load_manifest(datasource_type)

keyvault_client = None
keyvault = None
keyvault_subscription = None
keyvault_name = None
keyvault_rg = None
if manifest['supportSecretStoreAuthentication']:
cmd.command_kwargs['operation_group'] = 'vaults'
keyvault_update = False

from azure.cli.core.profiles import ResourceType
from azure.cli.command_modules.keyvault._client_factory import Clients, get_client

keyvault_params = parse_resource_id(keyvault_id)
keyvault_subscription = keyvault_params['subscription']
keyvault_name = keyvault_params['name']
keyvault_rg = keyvault_params['resource_group']

keyvault_client = getattr(get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_KEYVAULT, subscription_id=keyvault_subscription), Clients.vaults)

keyvault = keyvault_client.get(resource_group_name=keyvault_rg, vault_name=keyvault_name)

# Check if keyvault is not publicly accessible
if keyvault.properties.public_network_access == 'Disabled':
raise CLIError("Keyvault has public access disabled. Please enable public access, or grant access to your client IP")

# Check if the secret URI provided in backup instance is a valid secret
data_entity = get_client(cmd.cli_ctx, ResourceType.DATA_KEYVAULT)
data_client = data_entity.client_factory(cmd.cli_ctx, None)
secrets_list = data_client.get_secrets(vault_base_url=keyvault.properties.vault_uri)
given_secret_uri = backup_instance['properties']['datasource_auth_credentials']['secret_store_resource']['uri']
given_secret_id = helper.get_secret_params_from_uri(given_secret_uri)['secret_id']
valid_secret = False
for secret in secrets_list:
if given_secret_id == secret.id:
valid_secret = True
break

if not valid_secret:
raise CLIError("The secret URI provided in the --backup-instance is not associated with the "
"--keyvault-id provided. Please input a valid combination of secret URI and "
"--keyvault-id.")

keyvault_permission_models = manifest['secretStorePermissions']
if keyvault.properties.enable_rbac_authorization:
role = keyvault_permission_models['rbacModel']['roleDefinitionName']

keyvault_assignment_scope = helper.truncate_id_using_scope(keyvault_id, permissions_scope)

role_assignment = list_role_assignments(cmd, assignee=principal_id, role=role, scope=keyvault_id, include_inherited=True)
if not role_assignment:
assignment = create_role_assignment(cmd, assignee=principal_id, role=role, scope=keyvault_assignment_scope)
role_assignments_arr.append(helper.get_permission_object_from_role_object(assignment))

else:
from azure.cli.command_modules.keyvault.custom import set_policy
vault_secret_permissions = (keyvault_permission_models['vaultAccessPolicyModel']
['accessPolicies']
['permissions']
['secrets'])

secrets_array = []
for policy in keyvault.properties.access_policies:
if policy.object_id == principal_id:
secrets_array = policy.permissions.secrets
break

permissions_set = True
for permission in vault_secret_permissions:
if permission not in secrets_array:
permissions_set = False
secrets_array.append(permission)

if not permissions_set:
keyvault_update = True
keyvault = set_policy(cmd, keyvault_client, keyvault_rg, keyvault_name, object_id=principal_id, secret_permissions=secrets_array)
keyvault = keyvault.result()

from azure.cli.command_modules.keyvault.custom import update_vault_setter

if keyvault.properties.network_acls:
if keyvault.properties.network_acls.bypass == 'None':
keyvault_update = True
keyvault.properties.network_acls.bypass = 'AzureServices'
update_vault_setter(cmd, keyvault_client, keyvault, resource_group_name=keyvault_rg, vault_name=keyvault_name)

if keyvault_update:
role_assignments_arr.append(helper.get_permission_object_from_keyvault(keyvault))

for role_object in manifest['backupVaultPermissions']:
resource_id = helper.get_resource_id_from_backup_instance(backup_instance, role_object['type'])
resource_id = helper.truncate_id_using_scope(resource_id, "Resource")

assignment_scope = helper.truncate_id_using_scope(resource_id, permissions_scope)

role_assignments = list_role_assignments(cmd, assignee=principal_id, role=role_object['roleDefinitionName'],
scope=resource_id, include_inherited=True)
if not role_assignments:
assignment = create_role_assignment(cmd, assignee=principal_id, role=role_object['roleDefinitionName'],
scope=assignment_scope)
role_assignments_arr.append(helper.get_permission_object_from_role_object(assignment))

# Network line of sight access on server, if that is the datasource type
if datasource_type == 'AzureDatabaseForPostgreSQL':
server_params = parse_resource_id(backup_instance['properties']['data_source_info']['resource_id'])
server_sub = server_params['subscription']
server_name = server_params['name']
server_rg = server_params['resource_group']

from azure.mgmt.rdbms.postgresql import PostgreSQLManagementClient
postgres_firewall_client = getattr(get_mgmt_service_client(cmd.cli_ctx, PostgreSQLManagementClient, subscription_id=server_sub), 'firewall_rules')

firewall_rule_list = postgres_firewall_client.list_by_server(resource_group_name=server_rg, server_name=server_name)

allow_access_to_azure_ips = False
for rule in firewall_rule_list:
if rule.start_ip_address == rule.end_ip_address and rule.start_ip_address == '0.0.0.0':
allow_access_to_azure_ips = True
break

if not allow_access_to_azure_ips:
firewall_rule_name = 'AllowAllWindowsAzureIps'
parameters = {'name': firewall_rule_name, 'start_ip_address': '0.0.0.0', 'end_ip_address': '0.0.0.0'}

rule = postgres_firewall_client.begin_create_or_update(server_rg, server_name, firewall_rule_name, parameters)
role_assignments_arr.append(helper.get_permission_object_from_server_firewall_rule(rule.result()))

if not role_assignments_arr:
logger.warning("The required permissions are already assigned!")
else:
# Wait for 60 seconds to let the role assignments propagate
logger.warning("Waiting for 60 seconds for permissions to propagate")
time.sleep(60)

return role_assignments_arr


def dataprotection_job_list_from_resourcegraph(client, datasource_type, resource_groups=None, vaults=None,
subscriptions=None, start_time=None, end_time=None,
status=None, operation=None, datasource_id=None):
Expand Down
8 changes: 8 additions & 0 deletions src/dataprotection/azext_dataprotection/manual/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ def get_secret_store_type_values():
return ['AzureKeyVault']


def get_backup_operation_values():
return ['Backup']


def get_permission_scope_values():
return ['Resource', 'ResourceGroup', 'Subscription']


def get_resource_type_values():
return ['Microsoft.RecoveryServices/vaults']

Expand Down
Loading