Azure / azure-cli

Azure Command-Line Interface
MIT License
3.97k stars 2.95k forks source link

az synapse role assignment create fails when the user is not a Synapse Administrator #25790

Closed ghjklw closed 1 year ago

ghjklw commented 1 year ago

Command Name az synapse role assignment create

Describe the bug It should be possible to add a Synapse Administrator to Synapse RBAC if you are an owner of the Synapse workspace in Azure RBAC even though you are not a System Administrator. This works using Synapse Studio, but not using Azure CLI (or PowerShell module).

Sources:

This is relevant in a CI/CD context, to be able to automate the assignment of Synapse RBAC roles: there is no way with ARM or Bicep to control who is originally granted the "Synapse Administrator" role (stackoverflow)

Errors:

(Unauthorized) The principal 'x-x-x-x' does not have the required Synapse RBAC permission to perform this action. Required 
permission: Action: Microsoft.Synapse/workspaces/read, Scope: workspaces/XXXXXXXX/.
Code: Unauthorized
Message: The principal 'x-x-x-x' does not have the required Synapse RBAC permission to perform this action. Required permission: 
Action: Microsoft.Synapse/workspaces/read, Scope: workspaces/XXXX/.

To Reproduce

Expected behavior I would expect the role to be successfully assigned if the principal used has the "Owner" role for the workspace in Azure RBAC (or any other role with the ability to manage Azure RBAC), even though it does not have the permission Microsoft.Synapse/workspaces/read

Environment summary

Windows-10-10.0.22621-SP0
Python 3.10.10
Installer: MSI

azure-cli 2.46.0
ghjklw commented 1 year ago

To elaborate on this issue, this is not a limitation of the API or Python SDK. I can achieve the same role assignment using the following Python code:

from azure.synapse.accesscontrol import AccessControlClient
from azure.identity import DefaultAzureCredential
import uuid

# Workspace
workspace_name = 'xxxx'
principal_id = "xxxx"
synapse_analytics_endpoint = ".dev.azuresynapse.net"
role_id="6e4bf58a-b8e1-4cc3-bbf9-d73143322b78" # Synapse Administrator

# Acquire a credential object
credential = DefaultAzureCredential()

acc = AccessControlClient(
    credential=credential,
    endpoint='{}{}{}'.format("https://", workspace_name, synapse_analytics_endpoint)
)

acc.role_assignments.create_role_assignment(
    role_assignment_id=uuid.uuid5(),
    principal_id=principal_id,
    role_id=role_id,
    scope="workspaces/%s" % workspace_name
)
yonzhan commented 1 year ago

route to CXP team

mosharafMS commented 1 year ago

Same problem. I use bicep DeploymentScript task using the identity of user-assigned managed identity that is owner on the workspace (Azure RBAC) and fails to grant Synapse Administrator

ghjklw commented 1 year ago

Same problem. I use bicep DeploymentScript task using the identity of user-assigned managed identity that is owner on the workspace (Azure RBAC) and fails to grant Synapse Administrator

Exactly my use case 👍

I think I found the root cause: in the function _create_role_assignment a call is made to _resolve_role_id which in turns uses the function AccessControlClient.role_definitions.list_role_definitions(). This API requires the permission Microsoft.Synapse/workspaces/read and therefore fails. Interestingly the API AccessControlClient.role_definitions.get_role_definition_by_id does not require this permission...

The right fix would be to authorize this API for Azure RBAC owners of the Synapse workspace.

A simple temporary workaround is to assign the role by id (6e4bf58a-b8e1-4cc3-bbf9-d73143322b78) instead of role name (Synapse Administrator). The following command works:

az synapse role assignment create \
  --workspace-name $workspaceName \  
  --role "6e4bf58a-b8e1-4cc3-bbf9-d73143322b78" \  
  --assignee-object-id $principalId \  
  --assignee-principal-type User
mosharafMS commented 1 year ago

Thank you @ghjklw ! yes, your workaround works. I hit another issue that the command is not idempotent and fails the pipeline when executed twice, I have to check the existence of the role first. Created issue https://github.com/Azure/azure-cli/issues/25872

existingAssignment=$(az synapse role assignment list --workspace-name ${synapseName}  --role "6e4bf58a-b8e1-4cc3-bbf9-d73143322b78" --assignee ${adGroup} --only-show-errors);
if [ -z "$existingAssignment" ]; 
then az synapse role assignment create --workspace-name ${synapseName} --role "6e4bf58a-b8e1-4cc3-bbf9-d73143322b78" --assignee ${adGroup} --only-show-errors; 
fi
ghjklw commented 1 year ago

Thank you @ghjklw ! yes, your workaround works. I hit another issue that the command is not idempotent and fails the pipeline when executed twice, I have to check the existence of the role first. Created issue #25872

existingAssignment=$(az synapse role assignment list --workspace-name ${synapseName}  --role "6e4bf58a-b8e1-4cc3-bbf9-d73143322b78" --assignee ${adGroup} --only-show-errors);
if [ -z "$existingAssignment" ]; 
then az synapse role assignment create --workspace-name ${synapseName} --role "6e4bf58a-b8e1-4cc3-bbf9-d73143322b78" --assignee ${adGroup} --only-show-errors; 
fi

Sorry for the late reply! @mosharafMS, you're right, I also faced the same issue. Unfortunately, your workaround didn't work for me because az synapse role assignment list also requires the Microsoft.Synapse/workspaces/read RBAC.

Inspired by your solution, here's the workaround that I ended up using:

RESULT=$(az synapse role assignment create --workspace-name "$workspace" --role "$role" --assignee "$assignee" 2>&1)
EXIT_CODE=$?

if  [ "$EXIT_CODE" != 0 ]; then
    # If the error is of type "RoleAssignmentAlreadyExists", then it's fine
    if [[ "$RESULT" == *"RoleAssignmentAlreadyExists"* ]]; then
        echo "The Synapse role assignment already existed"
    else
        # If this was a different error, then print it to stderr and exit with the same error code
        echo $RESULT 1>&2
        exit $EXIT_CODE
    fi
fi
echo $RESULT

For anyone reading this thread, here's a more complete working solution with Bicep:

// Create the Synapse workspace
resource synapse 'Microsoft.Synapse/workspaces@2021-06-01' = {
  name: 'xxxx'
  location: location
  ...
}

// Create a managed identity used for the role assignment during the deployment
resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: 'xxxx'
  location: location
}

// Assign the Azure RBAC "Owner" role on the Synapse workspace to the managed identity just created
var roleDefinitionId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635') // Owner
resource identityRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(subscription().subscriptionId, userAssignedIdentity.properties.principalId, roleDefinitionId)
  scope: synapse
  properties: {
    principalId: userAssignedIdentity.properties.principalId
    principalType: 'ServicePrincipal'
    roleDefinitionId: roleDefinitionId
  }
}

// Assign the Synapse RBAC role "Synapse Adminitrator" to a given principalId
resource synapseRoleAssignRBAC 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
  name: 'synapseRoleAssignAZ-${uniqueString(synapse.name, principalId)}'
  kind: 'AzureCLI'
  location: location
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${userAssignedIdentity.properties.principalId}': {}
    }
  }
  properties: {
    forceUpdateTag: '1'
    azCliVersion: '2.42.0'
    retentionInterval: 'P1D'
    environmentVariables: [
      {
        name: 'subscription'
        value: subscription().subscriptionId
      }
      {
        name: 'workspace'
        value: synapse.name
      }
      {
        name: 'role'
        value: '6e4bf58a-b8e1-4cc3-bbf9-d73143322b78'
      }
      {
        name: 'assignee'
        value: principalId
      }
    ]
    scriptContent: '''
      # az synapse role assignment is not idempotent and the output must therefore be verified
      RESULT=$(az synapse role assignment create --workspace-name "$workspace" --role "$role" --assignee "$assignee" 2>&1)
      EXIT_CODE=$?

      if  [ "$EXIT_CODE" != 0 ]; then
          # If the error is of type "RoleAssignmentAlreadyExists", then it's fine
          if [[ "$RESULT" == *"RoleAssignmentAlreadyExists"* ]]; then
              echo "The Synapse role assignment already existed"
          else
              # If this was a different error, then print it to stderr and exit with the same error code
              echo $RESULT 1>&2
              exit $EXIT_CODE
          fi
      fi
      echo $RESULT
    '''
  }
}
ghost commented 1 year ago

Thanks for the feedback! We are routing this to the appropriate team for follow-up. cc @wonner, @kevinzz6.

Issue Details
**Command Name** az synapse role assignment create **Describe the bug** It should be possible to add a Synapse Administrator to Synapse RBAC if you are an owner of the Synapse workspace in Azure RBAC even though you are not a System Administrator. This works using Synapse Studio, but not using Azure CLI (or PowerShell module). Sources: * [Azure Synapse RBAC roles](https://learn.microsoft.com/en-us/azure/synapse-analytics/security/synapse-workspace-synapse-rbac-roles#built-in-synapse-rbac-roles-and-scopes): "In addition to Synapse Administrator, Azure Owners can also assign Synapse RBAC roles" * [How to manage Synapse RBAC role assignments in Synapse Studio](https://learn.microsoft.com/en-us/azure/synapse-analytics/security/how-to-manage-synapse-rbac-role-assignments): "To help you regain access to a workspace in the event that no Synapse Administrators are assigned or available to you, users with permissions to manage Azure RBAC role assignments on the workspace can also manage Synapse RBAC role assignments, allowing the addition of Synapse Administrator or other Synapse role assignments." This is relevant in a CI/CD context, to be able to automate the assignment of Synapse RBAC roles: there is no way with ARM or Bicep to control who is originally granted the "Synapse Administrator" role ([stackoverflow](https://stackoverflow.com/questions/72289140/there-is-a-way-to-set-up-synapse-administrators-with-a-bicep-module)) **Errors:** ``` (Unauthorized) The principal 'x-x-x-x' does not have the required Synapse RBAC permission to perform this action. Required permission: Action: Microsoft.Synapse/workspaces/read, Scope: workspaces/XXXXXXXX/. Code: Unauthorized Message: The principal 'x-x-x-x' does not have the required Synapse RBAC permission to perform this action. Required permission: Action: Microsoft.Synapse/workspaces/read, Scope: workspaces/XXXX/. ``` **To Reproduce** - Create a Synapse workspace - From Synapse studio, remove the "Synapse Administrator" role for your user account - Empty the cache / use an incognito window to confirm - run the command: ``` az synapse role assignment create \ --workspace-name $workspaceName \ --role "Synapse Administrator" \ --assignee-object-id $principalId \ --assignee-principal-type User ``` - Open Synapse Studio and confirm that you can assign the same role through the manage tab. **Expected behavior** I would expect the role to be successfully assigned if the principal used has the "Owner" role for the workspace in Azure RBAC (or any other role with the ability to manage Azure RBAC), even though it does not have the permission `Microsoft.Synapse/workspaces/read` **Environment summary** ``` Windows-10-10.0.22621-SP0 Python 3.10.10 Installer: MSI azure-cli 2.46.0 ```
Author: ghjklw
Assignees: -
Labels: `bug`, `Service Attention`, `customer-reported`, `Synapse`, `CXP Attention`, `Auto-Assign`
Milestone: -
wonner commented 1 year ago

Please create role assignment by role id directly if you are the Azure RBAC Owners, we will add it to cmdlet help doc.

jesperrula commented 9 months ago

I have tried to get the full Bicep example shown here to run. However, I get red squiggles for the references to "userAssignedIdentity.properties.principalId":

This expression is being used in an assignment to the "name" property of the "Microsoft.Authorization/roleAssignments" type, which requires a value that can be calculated at the start of the deployment. Properties of userAssignedIdentity which can be calculated at the start include "apiVersion", "id", "name", "type".bicep(BCP120)

Am I doing something wrong here? I basically just made a cut'n'paste of the full example in this thread.

My full code looks like this:

 param synapseWorkspaceName string
param managedResourceGroupName string
param administratorGroupName string
param administratorGroupId string
param location string = resourceGroup().location
param tags object = {}
@allowed([
  'None'
  'SystemAssigned'
  'SystemAssigned,UserAssigned'
])
param identity string = 'SystemAssigned'
param userAssignedIdentities object = {}
param allowAzureSynapseLinkforAzureSQLDatabaseToBypassFirewallRules bool = false
param initialWorkspaceAdminObjectId string = ''
param defaultDataLakeStorageName string
param createManagedPrivateEndpoint bool = false
param filesystem string = 'default'
param defaultDataLakeStorageResourceId string = ''
param encryption object = {}
param managedVirtualNetwork string = ''
param managedVirtualNetworkSettings object = {}
param privateEndpointConnections array = []
@allowed([
  'Enabled'
  'Disabled'
])
param publicNetworkAccess string = 'Disabled'
param purviewResourceId string = ''
param computeSubnetId string = ''
param workspaceRepositoryConfiguration object = {}

resource dst 'Microsoft.Storage/storageAccounts@2021-08-01' existing = {
  name: defaultDataLakeStorageName
}

resource synAdmins 'Microsoft.Synapse/workspaces/administrators@2021-06-01' = {
  name: 'activeDirectory'
  parent: syn
  properties: {
    administratorType: 'ActiveDirectory'
    login: administratorGroupName
    sid: administratorGroupId
    tenantId: subscription().tenantId
  }
}

//enables the option "Allow Azure Synapse Link for Azure SQL Database to bypass firewall rules"
resource networkByPass 'Microsoft.Synapse/workspaces/trustedServiceByPassConfiguration@2021-06-01-preview' = {
  name: 'default'
  parent: syn
  properties: {
    trustedServiceBypassEnabled: allowAzureSynapseLinkforAzureSQLDatabaseToBypassFirewallRules
  }
}

resource syn 'Microsoft.Synapse/workspaces@2021-06-01' = {
  name: synapseWorkspaceName
  location: location
  tags: tags
  identity: {
    type: identity
    userAssignedIdentities: empty(userAssignedIdentities) ? null : userAssignedIdentities
  }
  properties: {
      cspWorkspaceAdminProperties: empty(initialWorkspaceAdminObjectId) ? null : {
      initialWorkspaceAdminObjectId: initialWorkspaceAdminObjectId
    }
    defaultDataLakeStorage: {
      accountUrl: dst.properties.primaryEndpoints.dfs
      createManagedPrivateEndpoint: createManagedPrivateEndpoint
      filesystem: filesystem
      resourceId: empty(defaultDataLakeStorageResourceId) ? null : defaultDataLakeStorageResourceId
    }
    encryption: empty(encryption) ? null : encryption
    managedResourceGroupName: managedResourceGroupName
    managedVirtualNetwork: empty(managedVirtualNetwork) ? null : managedVirtualNetwork
    managedVirtualNetworkSettings: empty(managedVirtualNetworkSettings) ? null : managedVirtualNetworkSettings
    privateEndpointConnections: empty(privateEndpointConnections) ? null : privateEndpointConnections
    publicNetworkAccess: publicNetworkAccess
    purviewConfiguration: empty(purviewResourceId) ? null : {
      purviewResourceId: purviewResourceId
    }
    virtualNetworkProfile: empty(computeSubnetId) ? null : {
      computeSubnetId: computeSubnetId
    }
    workspaceRepositoryConfiguration: empty(workspaceRepositoryConfiguration) ? null : workspaceRepositoryConfiguration
  }
}

// Create a managed identity used for the role assignment during the deployment
resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: 'xxxx'
  location: location
}

// Assign the Azure RBAC "Owner" role on the Synapse workspace to the managed identity just created
var roleDefinitionId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635') // Owner
resource identityRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(subscription().subscriptionId, userAssignedIdentity.properties.principalId, roleDefinitionId)
  scope: syn
  properties: {
    principalId: userAssignedIdentity.properties.principalId
    principalType: 'ServicePrincipal'
    roleDefinitionId: roleDefinitionId
  }
}

// Assign the Synapse RBAC role "Synapse Adminitrator" to a given principalId
resource synapseRoleAssignRBAC 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
  name: 'synapseRoleAssignAZ'
  kind: 'AzureCLI'
  location: location
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${userAssignedIdentity.properties.principalId}': {
      }
    }
  }
  properties: {
    forceUpdateTag: '1'
    azCliVersion: '2.42.0'
    cleanupPreference: 'OnExpiration'
    retentionInterval: 'P1D'
    environmentVariables: [
      {
        name: 'subscription'
        value: subscription().subscriptionId
      }
      {
        name: 'workspace'
        value: syn.name
      }
      {
        name: 'role'
        value: '6e4bf58a-b8e1-4cc3-bbf9-d73143322b78'
      }
      {
        name: 'assignee'
        value: administratorGroupId
      }
    ]
    scriptContent: '''
      # az synapse role assignment is not idempotent and the output must therefore be verified
      RESULT=$(az synapse role assignment create --workspace-name "$workspace" --role "$role" --assignee "$assignee" 2>&1)
      EXIT_CODE=$?

      if  [ "$EXIT_CODE" != 0 ]; then
          # If the error is of type "RoleAssignmentAlreadyExists", then it's fine
          if [[ "$RESULT" == *"RoleAssignmentAlreadyExists"* ]]; then
              echo "The Synapse role assignment already existed"
          else
              # If this was a different error, then print it to stderr and exit with the same error code
              echo $RESULT 1>&2
              exit $EXIT_CODE
          fi
      fi
      echo $RESULT
    '''
  }
}

output principalId string = syn.identity.principalId
output name string = syn.name
output id string = syn.id
wonner commented 9 months ago

Hi @jesperrula data plane resources like role assignment does not support bicep.

ohads-MSFT commented 2 weeks ago

Please create role assignment by role id directly if you are the Azure RBAC Owners, we will add it to cmdlet help doc.

This role is not Azure RBAC, the proper solution here would be to add support for it in native Synapse bicep - any chance you could get in touch with the product team about it?