Thank you Daniel Mann. Posting your suggestion as an answer to help other community members.
As Daniel suggested, "Don't give users Contributor
permissions and do all deployments through service principals."
You can add a role assignment to an existing service principal using ARM templates.
Subscription deployments with ARM templates
For example:
{
"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.4.1.14562",
"templateHash": "17666923867108240565"
}
},
"parameters": {
"rgName": {
"type": "string",
"metadata": {
"description": "Name of the resourceGroup to create"
}
},
"rgLocation": {
"type": "string",
"metadata": {
"description": "Location for the resourceGroup"
}
},
"principalId": {
"type": "string",
"metadata": {
"description": "principalId of the user that will be given contributor access to the resourceGroup"
}
},
"roleDefinitionId": {
"type": "string",
"defaultValue": "b24988ac-6180-42a0-ab88-20f7382dd24c",
"metadata": {
"description": "roleDefinition to apply to the resourceGroup - default is contributor"
}
},
"roleAssignmentName": {
"type": "string",
"defaultValue": "[guid(parameters('principalId'), parameters('roleDefinitionId'), parameters('rgName'))]",
"metadata": {
"description": "Unique name for the roleAssignment in the format of a guid"
}
}
},
"functions": [],
"resources": [
{
"type": "Microsoft.Resources/resourceGroups",
"apiVersion": "2019-10-01",
"name": "[parameters('rgName')]",
"location": "[parameters('rgLocation')]",
"tags": {
"Note": "subscription level deployment"
},
"properties": {}
},
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2019-10-01",
"name": "applyLock",
"resourceGroup": "[parameters('rgName')]",
"properties": {
"expressionEvaluationOptions": {
"scope": "inner"
},
"mode": "Incremental",
"parameters": {
"principalId": {
"value": "[parameters('principalId')]"
},
"roleDefinitionId": {
"value": "[parameters('roleDefinitionId')]"
},
"roleAssignmentName": {
"value": "[parameters('roleAssignmentName')]"
}
},
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.4.1.14562",
"templateHash": "12740078983609655962"
}
},
"parameters": {
"principalId": {
"type": "string",
"metadata": {
"description": "principalId of the user that will be given contributor access to the resourceGroup"
}
},
"roleDefinitionId": {
"type": "string",
"metadata": {
"description": "roleDefinition to apply to the resourceGroup - default is contributor"
}
},
"roleAssignmentName": {
"type": "string",
"metadata": {
"description": "Unique name for the roleAssignment in the format of a guid"
}
}
},
"functions": [],
"resources": [
{
"type": "Microsoft.Authorization/locks",
"apiVersion": "2016-09-01",
"name": "DontDelete",
"properties": {
"level": "CanNotDelete",
"notes": "Prevent deletion of the resourceGroup"
}
},
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2020-04-01-preview",
"name": "[guid(parameters('roleAssignmentName'))]",
"properties": {
"roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionId'))]",
"principalId": "[parameters('principalId')]"
}
}
]
}
},
"dependsOn": [
"[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('rgName'))]"
]
}
]
}
You can refer to Is there a way to use ARM Template to create an Azure Service Principal?, Assign Azure roles using Azure Resource Manager templates, and Automatic Service Principal creation in the Azure deployment