diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/README.md b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/README.md index ac759f61a..cb6ce7b66 100644 --- a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/README.md +++ b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/README.md @@ -1,131 +1,120 @@ -# LicenseType-SQLArc +# Arc-enabled SQL Server license type configuration with Azure Policy -This Azure Policy ensures that all SQL Arc servers have the required `LicenseType` value. Servers that do not match the required license type are marked as non-compliant. The remediation task sets `LicenseType` to the value specified by the `requiredLicenseType` parameter. +This repo deploys and remediates a custom Azure Policy that configures and enforces Arc-enabled SQL Server extension `LicenseType` to a selected target value (for example `Paid` or `PAYG`). -Use Azure CLI or PowerShell to create the policy definition. +## What Is In This Repo -## Artifacts +- `policy/azurepolicy.json`: Custom policy definition (DeployIfNotExists). +- `scripts/deployment.ps1`: Creates/updates the policy definition and policy assignment. +- `scripts/start-remediation.ps1`: Starts a remediation task for the created assignment. +- `docs/screenshots/`: Visual references. -- **policy.json**: Main policy definition referencing external parameter and rule files. -- **params.json**: Defines policy parameters. -- **rules.json**: Contains the policy rule logic. +## Prerequisites -## Copy policy artifacts to your environment +- PowerShell with Az modules installed (`Az.Resources`). +- Logged in to Azure (`Connect-AzAccount`). +- Permissions to create policy definitions/assignments and remediation tasks at target scope. -```PowerShell +## Deploy Policy -curl https://raw.githubusercontent.com/microsoft/sql-server-samples/refs/heads/master/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/params.json -o params.json -curl https://raw.githubusercontent.com/microsoft/sql-server-samples/refs/heads/master/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/rules.json -o rules.json +Parameter reference: -``` +| Parameter | Required | Default | Allowed values | Description | +|---|---|---|---|---| +| `ManagementGroupId` | Yes | N/A | Any valid management group ID | Scope where the policy definition is created. | +| `ExtensionType` | No | `Both` | `Windows`, `Linux`, `Both` | Targets the Arc SQL extension platform. When `Both` (default), a single policy definition and assignment covers both platforms. When a specific type is selected, the naming and scope are tailored to that platform. | +| `SubscriptionId` | No | Not set | Any valid subscription ID | If provided, policy assignment scope is the subscription. | +| `TargetLicenseType` | Yes | N/A | `Paid`, `PAYG` | Target `LicenseType` value to enforce. | +| `LicenseTypesToOverwrite` | No | All | `Unspecified`, `Paid`, `PAYG`, `LicenseOnly` | Select which current license states are eligible for update. Use `Unspecified` to include resources with no `LicenseType` configured. | -## Create policy +Definition and assignment creation: -Use the following command to create policy +1. Clone the repo. -```PowerShell +```powershell +git clone https://github.com/microsoft/sql-server-samples.git +cd sql-server-samples/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance +``` -$SubId = "" -$PolicyName = "LicenseType-SQLArc" +2. Login to Azure. -az policy definition create ` - --name $PolicyName ` - --display-name $PolicyName ` - --description "This Azure Policy ensures that all SQL Arc servers have the required LicenseType. Servers that do not match are marked as non-compliant and remediated to the required license type." ` - --rules "@rules.json" ` - --params "@params.json" ` - --mode Indexed ` - --subscription $SubId ` - --only-show-errors | Out-Null +```powershell +Connect-AzAccount ``` -## Assign policy +```powershell +# Example: target both platforms (default) +.\scripts\deployment.ps1 -ManagementGroupId "" -SubscriptionId "" -TargetLicenseType "PAYG" -LicenseTypesToOverwrite @("Paid") -Use the following command to assign policy +# Example: target only Linux +.\scripts\deployment.ps1 -ManagementGroupId "" -ExtensionType "Linux" -SubscriptionId "" -TargetLicenseType "PAYG" -LicenseTypesToOverwrite @("Paid") +``` +The first example (without `-ExtensionType`) will: +* Create/update a single policy definition and assignment covering **both** Windows and Linux. +* Assign that policy at the specified subscription scope. +* Enforce LicenseType = PAYG. +* Update only resources where current `LicenseType` is `Paid`. -```PowerShell +The second example creates a Linux-specific definition and assignment, with platform-tailored naming. -$SubId = "" -$RgName = "" # optional; set to "" to target subscription scope -$Location = "" # e.g., eastus, westus2 -$RequiredLicenseType = "PAYG" # e.g., PAYG, LicenseOnly +Scenario examples: -if ([string]::IsNullOrWhiteSpace($RgName)) { - $Scope = "/subscriptions/$SubId" -} else { - $Scope = "/subscriptions/$SubId/resourceGroups/$RgName" -} +```powershell +# Target Paid, both Linux and Windows, but only for resources with missing LicenseType or LicenseOnly (do not target PAYG) +.\scripts\deployment.ps1 -ManagementGroupId "" -TargetLicenseType "Paid" -LicenseTypesToOverwrite @("Unspecified","LicenseOnly") -az account set --subscription $SubId +# Target PAYG, but only where current LicenseType is Paid (do not target missing or LicenseOnly) +.\scripts\deployment.ps1 -ManagementGroupId "" -ExtensionType "Linux" -TargetLicenseType "PAYG" -LicenseTypesToOverwrite @("Paid") -az policy assignment create ` - --name "LicenseType-SQLArc-Assign" ` - --policy "LicenseType-SQLArc" ` - --scope "$Scope" ` - --params "{ \"effect\": { \"value\": \"DeployIfNotExists\" }, \"requiredLicenseType\": { \"value\": \"$RequiredLicenseType\" } }" ` - --mi-system-assigned ` - --role "Contributor" ` - --identity-scope "$Scope" ` - --location "$Location" ` - --only-show-errors | Out-Null +# Overwrite all known existing LicenseType values (Paid, PAYG, LicenseOnly), but not missing +.\scripts\deployment.ps1 -ManagementGroupId "" -ExtensionType "Linux" -TargetLicenseType "Paid" -LicenseTypesToOverwrite @("Paid","PAYG","LicenseOnly") ``` -## Create remediation task - -Use the following command to create a remediation task - -```PowerShell - -$RemediationName = "Remediate-LicenseType-SQLArc" -$PolicyAssignmentName = "LicenseType-SQLArc-Assign" -$SubId = "" -$RgName = "" - -az account set --subscription $SubId - -if ([string]::IsNullOrWhiteSpace($RgName)) { - az policy remediation create ` - --name $RemediationName ` - --policy-assignment $PolicyAssignmentName ` - --resource-discovery-mode ReEvaluateCompliance ` - --only-show-errors | Out-Null -} else { - az policy remediation create ` - --name $RemediationName ` - --policy-assignment $PolicyAssignmentName ` - --resource-group "$RgName" ` - --resource-discovery-mode ReEvaluateCompliance ` - --only-show-errors | Out-Null -} -``` +Note: `scripts/deployment.ps1` automatically grants required roles to the policy assignment managed identity at assignment scope, preventing common `PolicyAuthorizationFailed` errors during DeployIfNotExists deployments. + +## Start Remediation -## Remove remediation task - -```PowerShell - -$RemediationName = "Remediate-LicenseType-SQLArc" -$RgName = "" -$SubId = "" - -if ([string]::IsNullOrWhiteSpace($RgName)) { - az policy remediation cancel ` - --name $RemediationName ` - --subscription $SubId ` - --only-show-errors | Out-Null - az policy remediation delete ` - --name $RemediationName ` - --subscription $SubId ` - --only-show-errors | Out-Null -} else { - az policy remediation cancel ` - --name $RemediationName ` - --resource-group $RgName ` - --subscription $SubId ` - --only-show-errors | Out-Null - az policy remediation delete ` - --name $RemediationName ` - --resource-group $RgName ` - --subscription $SubId ` - --only-show-errors | Out-Null -} +Parameter reference: + +| Parameter | Required | Default | Allowed values | Description | +|---|---|---|---|---| +| `ManagementGroupId` | Yes | N/A | Any valid management group ID | Used to resolve the policy definition/assignment naming context. | +| `ExtensionType` | No | `Both` | `Windows`, `Linux`, `Both` | Must match the platform used for the assignment. When `Both` (default), remediates the combined assignment. | +| `SubscriptionId` | No | Not set | Any valid subscription ID | If provided, remediation runs at subscription scope. | +| `TargetLicenseType` | Yes | N/A | `Paid`, `PAYG` | Must match the assignment target license type. | +| `GrantMissingPermissions` | No | `false` | Switch (`present`/`not present`) | If set, checks and assigns missing required roles before remediation. | + +```powershell +# Example: remediate both platforms (default) +.\scripts\start-remediation.ps1 -ManagementGroupId "" -SubscriptionId "" -TargetLicenseType "PAYG" -GrantMissingPermissions + +# Example: remediate only Linux +.\scripts\start-remediation.ps1 -ManagementGroupId "" -ExtensionType "Linux" -SubscriptionId "" -TargetLicenseType "PAYG" -GrantMissingPermissions ``` + +## Managed Identity And Roles + +The policy assignment is created with `-IdentityType SystemAssigned`. Azure creates a managed identity on the assignment and uses it to apply DeployIfNotExists changes during enforcement and remediation. + +Required roles: + +- `Azure Extension for SQL Server Deployment` (`7392c568-9289-4bde-aaaa-b7131215889d`) +- `Reader` (`acdd72a7-3385-48ef-bd42-f606fba81ae7`) +- `Resource Policy Contributor` (required so DeployIfNotExists can create template deployments) + +## Troubleshooting + +If you see `PolicyAuthorizationFailed`, the policy assignment identity is missing one or more required roles at assignment scope (or inherited scope), often causing missing `Microsoft.HybridCompute/machines/extensions/write` permission. + +Use one of these options: + +- Re-run `scripts/deployment.ps1` (default behavior assigns `Resource Policy Contributor` automatically). +- Re-run `scripts/deployment.ps1` (default behavior assigns required roles automatically). +- Run `scripts/start-remediation.ps1 -GrantMissingPermissions` (checks and assigns missing required roles before remediation). + +## Screenshots + +![overview](./docs/screenshots/overview.png) +![pre-policy](./docs/screenshots/pre-policy.png) +![remediation](./docs/screenshots/remediation.png) +![postpolicy](./docs/screenshots/post-policy.png) \ No newline at end of file diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/docs/screenshots/overview.png b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/docs/screenshots/overview.png new file mode 100644 index 000000000..8eac1538b Binary files /dev/null and b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/docs/screenshots/overview.png differ diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/docs/screenshots/post-policy.png b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/docs/screenshots/post-policy.png new file mode 100644 index 000000000..77bd70a63 Binary files /dev/null and b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/docs/screenshots/post-policy.png differ diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/docs/screenshots/pre-policy.png b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/docs/screenshots/pre-policy.png new file mode 100644 index 000000000..a4a3e8298 Binary files /dev/null and b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/docs/screenshots/pre-policy.png differ diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/docs/screenshots/remediation.png b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/docs/screenshots/remediation.png new file mode 100644 index 000000000..5a203e009 Binary files /dev/null and b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/docs/screenshots/remediation.png differ diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/params.json b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/params.json deleted file mode 100644 index 603eebb7e..000000000 --- a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/params.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "effect": { - "type": "String", - "metadata": { - "displayName": "Effect", - "description": "Enable or disable the execution of the policy." - }, - "allowedValues": [ - "DeployIfNotExists", - "Disabled" - ], - "defaultValue": "DeployIfNotExists" - }, - "requiredLicenseType": { - "type": "String", - "metadata": { - "displayName": "Required License Type", - "description": "The license type that SQL Arc servers must have to be considered compliant." - }, - "allowedValues": [ - "PAYG", - "Paid", - "LicenseOnly" - ], - "defaultValue": "PAYG" - } -} diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/policy.json b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/policy.json deleted file mode 100644 index 420a7dc93..000000000 --- a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/policy.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "properties": { - "displayName": "LicenseType-SQLArc", - "policyType": "Custom", - "mode": "Indexed", - "description": "Policy to ensure all SQL Arc servers have the required LicenseType. Servers that do not match are marked as non-compliant and remediated to the required license type.", - "metadata": { - "category": "SQLArc", - "version": "1.0.0" - }, - "parameters": "./params.json", - "policyRule": "./rules.json" - } -} diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/policy/azurepolicy.json b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/policy/azurepolicy.json new file mode 100644 index 000000000..5450371d0 --- /dev/null +++ b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/policy/azurepolicy.json @@ -0,0 +1,263 @@ +{ + "displayName": "Set Arc-enabled SQL Server license type to 'License With Software Assurance'", + "policyType": "Custom", + "mode": "Indexed", + "description": "This policy sets the license type for Arc-enabled SQL Server to 'License With Software Assurance'. ", + "metadata": { + "category": "" + }, + "version": "1.0.0", + "parameters": { + "effect": { + "type": "String", + "metadata": { + "displayName": "Effect", + "description": "Enable or disable the execution of the policy." + }, + "allowedValues": [ + "DeployIfNotExists", + "Disabled" + ], + "defaultValue": "DeployIfNotExists" + }, + "sqlServerExtensionTypes": { + "type": "Array", + "metadata": { + "displayName": "SQL Server extension types", + "description": "Arc-enabled SQL Server extension names to target." + }, + "allowedValues": [ + "WindowsAgent.SqlServer", + "LinuxAgent.SqlServer" + ], + "defaultValue": [ + "WindowsAgent.SqlServer", + "LinuxAgent.SqlServer" + ] + }, + "targetLicenseType": { + "type": "String", + "metadata": { + "displayName": "Target license type", + "description": "LicenseType value to enforce on the Arc-enabled SQL Server extension settings." + }, + "allowedValues": [ + "Paid", + "PAYG" + ], + "defaultValue": "Paid" + }, + "licenseTypesToOverwrite": { + "type": "Array", + "metadata": { + "displayName": "Current license types to overwrite", + "description": "Select which current LicenseType states are eligible for update. Use 'Unspecified' to include resources where LicenseType is missing." + }, + "allowedValues": [ + "Unspecified", + "Paid", + "PAYG", + "LicenseOnly" + ], + "defaultValue": [ + "Unspecified", + "Paid", + "PAYG", + "LicenseOnly" + ] + } + }, + "policyRule": { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.HybridCompute/machines/extensions" + }, + { + "anyOf": [ + { + "field": "name", + "in": "[parameters('sqlServerExtensionTypes')]" + }, + { + "field": "name", + "like": "*Agent.SqlServer" + } + ] + }, + { + "field": "Microsoft.HybridCompute/machines/extensions/type", + "in": "[parameters('sqlServerExtensionTypes')]" + } + ] + }, + "then": { + "effect": "[parameters('effect')]", + "details": { + "type": "Microsoft.HybridCompute/machines/extensions", + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/7392c568-9289-4bde-aaaa-b7131215889d", + "/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7" + ], + "name": "[field('fullName')]", + "existenceCondition": { + "anyOf": [ + { + "allOf": [ + { + "field": "Microsoft.HybridCompute/machines/extensions/settings", + "ContainsKey": "LicenseType" + }, + { + "value": "[length(intersection(field('Microsoft.HybridCompute/machines/extensions/settings'), createObject('LicenseType', parameters('targetLicenseType'))))]", + "equals": 1 + } + ] + }, + { + "allOf": [ + { + "field": "Microsoft.HybridCompute/machines/extensions/settings", + "notContainsKey": "LicenseType" + }, + { + "value": "[contains(parameters('licenseTypesToOverwrite'), 'Unspecified')]", + "equals": false + } + ] + }, + { + "allOf": [ + { + "field": "Microsoft.HybridCompute/machines/extensions/settings", + "ContainsKey": "LicenseType" + }, + { + "value": "[length(intersection(field('Microsoft.HybridCompute/machines/extensions/settings'), createObject('LicenseType', 'Paid')))]", + "equals": 1 + }, + { + "value": "[contains(parameters('licenseTypesToOverwrite'), 'Paid')]", + "equals": false + } + ] + }, + { + "allOf": [ + { + "field": "Microsoft.HybridCompute/machines/extensions/settings", + "ContainsKey": "LicenseType" + }, + { + "value": "[length(intersection(field('Microsoft.HybridCompute/machines/extensions/settings'), createObject('LicenseType', 'PAYG')))]", + "equals": 1 + }, + { + "value": "[contains(parameters('licenseTypesToOverwrite'), 'PAYG')]", + "equals": false + } + ] + }, + { + "allOf": [ + { + "field": "Microsoft.HybridCompute/machines/extensions/settings", + "ContainsKey": "LicenseType" + }, + { + "value": "[length(intersection(field('Microsoft.HybridCompute/machines/extensions/settings'), createObject('LicenseType', 'LicenseOnly')))]", + "equals": 1 + }, + { + "value": "[contains(parameters('licenseTypesToOverwrite'), 'LicenseOnly')]", + "equals": false + } + ] + } + ] + }, + "evaluationDelay": "AfterProvisioningSuccess", + "deployment": { + "properties": { + "mode": "incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "extensionName": { + "type": "string", + "metadata": { + "description": "The Resource name of the Arc server extension." + } + }, + "vmLocation": { + "type": "string", + "metadata": { + "description": "The location of the Arc server." + } + }, + "agentName": { + "type": "string", + "metadata": { + "description": "Name of the agent, i.e. WindowsAgent.SQLServer." + } + }, + "existingSettings": { + "type": "object", + "metadata": { + "description": "The existing settings on the extension." + } + }, + "targetLicenseType": { + "type": "string", + "metadata": { + "description": "LicenseType value to set on the extension." + } + } + }, + "functions": [], + "variables": { + "vmExtensionPublisher": "Microsoft.AzureData", + "licenseSettings": { + "LicenseType": "[parameters('targetLicenseType')]" + } + }, + "resources": [ + { + "name": "[parameters('extensionName')]", + "type": "Microsoft.HybridCompute/machines/extensions", + "location": "[parameters('vmLocation')]", + "apiVersion": "2022-11-10", + "properties": { + "publisher": "[variables('vmExtensionPublisher')]", + "type": "[parameters('agentName')]", + "settings": "[union(parameters('existingSettings'), variables('licenseSettings'))]" + } + } + ], + "outputs": {} + }, + "parameters": { + "extensionName": { + "value": "[field('fullName')]" + }, + "vmLocation": { + "value": "[field('location')]" + }, + "agentName": { + "value": "[field('name')]" + }, + "existingSettings": { + "value": "[field('Microsoft.HybridCompute/machines/extensions/settings')]" + }, + "targetLicenseType": { + "value": "[parameters('targetLicenseType')]" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/rules.json b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/rules.json deleted file mode 100644 index 3d487ec90..000000000 --- a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/rules.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "if": { - "allOf": [ - { - "equals": "Microsoft.HybridCompute/machines/extensions", - "field": "type" - }, - { - "equals": "Microsoft.AzureData", - "field": "Microsoft.HybridCompute/machines/extensions/publisher" - }, - { - "equals": "WindowsAgent.SqlServer", - "field": "Microsoft.HybridCompute/machines/extensions/type" - } - ] - }, - "then": { - "effect": "[parameters('effect')]", - "details": { - "type": "Microsoft.HybridCompute/machines/extensions", - "roleDefinitionIds": [ - "/providers/Microsoft.Authorization/roleDefinitions/7392c568-9289-4bde-aaaa-b7131215889d", - "/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7" - ], - "name": "[field('fullName')]", - "existenceCondition": { - "equals": "[string(createObject('LicenseType', parameters('requiredLicenseType')))]", - "value": "[string(intersection(if(empty(field('Microsoft.HybridCompute/machines/extensions/settings')), createObject(), field('Microsoft.HybridCompute/machines/extensions/settings')), createObject('LicenseType', parameters('requiredLicenseType'))))]" - }, - "evaluationDelay": "AfterProvisioningSuccess", - "deployment": { - "properties": { - "mode": "incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "extensionName": { - "type": "string" - }, - "vmLocation": { - "type": "string" - }, - "agentName": { - "type": "string" - }, - "existingSettings": { - "type": "object" - }, - "requiredLicenseType": { - "type": "string" - } - }, - "variables": { - "vmExtensionPublisher": "Microsoft.AzureData", - "updatedSettings": { - "LicenseType": "[parameters('requiredLicenseType')]" - } - }, - "resources": [ - { - "name": "[parameters('extensionName')]", - "type": "Microsoft.HybridCompute/machines/extensions", - "location": "[parameters('vmLocation')]", - "apiVersion": "2022-11-10", - "properties": { - "publisher": "[variables('vmExtensionPublisher')]", - "type": "[parameters('agentName')]", - "settings": "[union(parameters('existingSettings'), variables('updatedSettings'))]" - } - } - ] - }, - "parameters": { - "extensionName": { - "value": "[field('fullName')]" - }, - "vmLocation": { - "value": "[field('location')]" - }, - "agentName": { - "value": "[field('name')]" - }, - "existingSettings": { - "value": "[field('Microsoft.HybridCompute/machines/extensions/settings')]" - }, - "requiredLicenseType": { - "value": "[parameters('requiredLicenseType')]" - } - } - } - } - } - } -} diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/scripts/deployment.ps1 b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/scripts/deployment.ps1 new file mode 100644 index 000000000..98aab3392 --- /dev/null +++ b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/scripts/deployment.ps1 @@ -0,0 +1,124 @@ +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$ManagementGroupId, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$SubscriptionId, + + [Parameter(Mandatory = $false)] + [ValidateSet('Windows', 'Linux', 'Both')] + [string]$ExtensionType = 'Both', + + [Parameter(Mandatory = $true)] + [ValidateSet('Paid', 'PAYG')] + [string]$TargetLicenseType, + + [Parameter(Mandatory = $false)] + [ValidateSet('Unspecified', 'Paid', 'PAYG', 'LicenseOnly')] + [string[]]$LicenseTypesToOverwrite = @('Unspecified', 'Paid', 'PAYG', 'LicenseOnly'), + + [Parameter(Mandatory = $false)] + [switch]$SkipManagedIdentityRoleAssignment +) + +$AssignmentScope = "/providers/Microsoft.Management/managementGroups/$ManagementGroupId" + +if ($PSBoundParameters.ContainsKey('SubscriptionId')) { + $AssignmentScope = "/subscriptions/$SubscriptionId" +} + +$ExtensionTypes = if ($ExtensionType -eq 'Both') { + @('Windows', 'Linux') +} +else { + @($ExtensionType) +} + +$SqlServerExtensionTypes = $ExtensionTypes | ForEach-Object { + if ($_ -eq 'Linux') { 'LinuxAgent.SqlServer' } else { 'WindowsAgent.SqlServer' } +} + +$PolicyJsonPath = Join-Path $PSScriptRoot '..\policy\azurepolicy.json' +$LicenseToken = if ($TargetLicenseType -eq 'PAYG') { 'payg' } else { 'sa' } + +if ($ExtensionType -eq 'Both') { + $PlatformToken = 'all' + $PlatformLabel = 'All platforms' +} +else { + $PlatformToken = $ExtensionType.ToLowerInvariant() + $PlatformLabel = $ExtensionType +} + +$PolicyDefinitionName = "activate-sql-arc-$LicenseToken-$PlatformToken" +$PolicyAssignmentName = "sql-arc-$LicenseToken-$PlatformToken" + +if ($TargetLicenseType -eq 'PAYG') { + $PolicyDefinitionDisplayName = "Arc-enabled SQL Server ($PlatformLabel) license type to 'Pay-as-you-go'" + $PolicyAssignmentDisplayName = "Arc-enabled SQL Server ($PlatformLabel) license type to 'Pay-as-you-go'" +} +else { + $PolicyDefinitionDisplayName = "Set Arc-enabled SQL Server ($PlatformLabel) license type to 'License With Software Assurance'" + $PolicyAssignmentDisplayName = "Set Arc-enabled SQL Server ($PlatformLabel) license type to 'License With Software Assurance'" +} + +#Create policy definition +New-AzPolicyDefinition ` + -Name $PolicyDefinitionName ` + -DisplayName $PolicyDefinitionDisplayName ` + -Policy $PolicyJsonPath ` + -ManagementGroupName $ManagementGroupId ` + -Mode Indexed ` + -ErrorAction Stop + +#Assign policy definition +$Policy = Get-AzPolicyDefinition -Name $PolicyDefinitionName -ManagementGroupName $ManagementGroupId +$PolicyAssignment = New-AzPolicyAssignment ` + -Name $PolicyAssignmentName ` + -DisplayName $PolicyAssignmentDisplayName ` + -PolicyDefinition $Policy ` + -PolicyParameterObject @{ + sqlServerExtensionTypes = $SqlServerExtensionTypes + targetLicenseType = $TargetLicenseType + licenseTypesToOverwrite = $LicenseTypesToOverwrite + } ` + -Scope $AssignmentScope ` + -Location 'westeurope' ` + -IdentityType 'SystemAssigned' ` + -ErrorAction Stop + +if (-not $SkipManagedIdentityRoleAssignment) { + $requiredRoleNames = @( + 'Azure Extension for SQL Server Deployment' + 'Reader' + 'Resource Policy Contributor' + ) + $principalId = $PolicyAssignment.IdentityPrincipalId + + if ([string]::IsNullOrEmpty($principalId)) { + throw "Policy assignment identity principal ID is empty. Cannot assign required roles." + } + + foreach ($requiredRoleName in $requiredRoleNames) { + $existingRole = Get-AzRoleAssignment ` + -ObjectId $principalId ` + -RoleDefinitionName $requiredRoleName ` + -Scope $AssignmentScope ` + -ErrorAction SilentlyContinue + + if (-not $existingRole) { + New-AzRoleAssignment ` + -ObjectId $principalId ` + -RoleDefinitionName $requiredRoleName ` + -Scope $AssignmentScope ` + -ErrorAction Stop | Out-Null + + Write-Output "Assigned '$requiredRoleName' to policy assignment identity ($principalId) at scope $AssignmentScope." + } + else { + Write-Output "Policy assignment identity already has '$requiredRoleName' at scope $AssignmentScope." + } + } +} \ No newline at end of file diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/scripts/start-remediation.ps1 b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/scripts/start-remediation.ps1 new file mode 100644 index 000000000..b2f895c0b --- /dev/null +++ b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/scripts/start-remediation.ps1 @@ -0,0 +1,126 @@ +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$ManagementGroupId, + + [Parameter(Mandatory = $false)] + [ValidateSet('Windows', 'Linux', 'Both')] + [string]$ExtensionType = 'Both', + + [Parameter(Mandatory = $true)] + [ValidateSet('Paid', 'PAYG')] + [string]$TargetLicenseType, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$SubscriptionId, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$PolicyAssignmentName, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$RemediationName, + + [Parameter(Mandatory = $false)] + [ValidateSet('ExistingNonCompliant', 'ReEvaluateCompliance')] + [string]$ResourceDiscoveryMode, + + [Parameter(Mandatory = $false)] + [switch]$GrantMissingPermissions +) + +$AssignmentScope = "/providers/Microsoft.Management/managementGroups/$ManagementGroupId" + +if ($PSBoundParameters.ContainsKey('SubscriptionId')) { + $AssignmentScope = "/subscriptions/$SubscriptionId" +} + +$LicenseToken = if ($TargetLicenseType -eq 'PAYG') { 'payg' } else { 'sa' } + +if ($ExtensionType -eq 'Both') { + $PlatformToken = 'all' +} +else { + $PlatformToken = $ExtensionType.ToLowerInvariant() +} + +if (-not $PSBoundParameters.ContainsKey('PolicyAssignmentName')) { + $PolicyAssignmentName = "sql-arc-$LicenseToken-$PlatformToken" +} + +if (-not $PSBoundParameters.ContainsKey('RemediationName')) { + $RemediationName = "remediate-sql-arc-$LicenseToken-$PlatformToken" +} + +if (-not $PSBoundParameters.ContainsKey('ResourceDiscoveryMode')) { + if ($PSBoundParameters.ContainsKey('SubscriptionId')) { + $ResourceDiscoveryMode = 'ReEvaluateCompliance' + } + else { + $ResourceDiscoveryMode = 'ExistingNonCompliant' + } +} + +# Validate assignment exists before creating remediation. +$PolicyAssignmentObj = Get-AzPolicyAssignment -Scope $AssignmentScope -Name $PolicyAssignmentName -ErrorAction Stop + +$requiredRoleNames = @( + 'Azure Extension for SQL Server Deployment' + 'Reader' + 'Resource Policy Contributor' +) +$principalId = $PolicyAssignmentObj.IdentityPrincipalId + +if ([string]::IsNullOrEmpty($principalId)) { + throw "Policy assignment identity principal ID is empty. Cannot verify required roles." +} + +$missingRoles = @() + +foreach ($requiredRoleName in $requiredRoleNames) { + $requiredRole = Get-AzRoleAssignment ` + -ObjectId $principalId ` + -RoleDefinitionName $requiredRoleName ` + -Scope $AssignmentScope ` + -ErrorAction SilentlyContinue + + if (-not $requiredRole) { + $missingRoles += $requiredRoleName + } +} + +if ($missingRoles.Count -gt 0) { + if ($GrantMissingPermissions) { + foreach ($missingRole in $missingRoles) { + New-AzRoleAssignment ` + -ObjectId $principalId ` + -RoleDefinitionName $missingRole ` + -Scope $AssignmentScope ` + -ErrorAction Stop | Out-Null + + Write-Output "Assigned '$missingRole' to policy assignment identity ($principalId) at scope $AssignmentScope." + } + } + else { + throw "Missing required roles [$($missingRoles -join ', ')] for policy assignment identity ($principalId) at scope $AssignmentScope. Re-run with -GrantMissingPermissions or assign the roles manually." + } +} + +$CommonParams = @{ + Name = $RemediationName + PolicyAssignmentId = $PolicyAssignmentObj.Id + Scope = $AssignmentScope + ResourceDiscoveryMode = $ResourceDiscoveryMode +} + +if (Get-Command -Name Start-AzPolicyRemediation -ErrorAction SilentlyContinue) { + Start-AzPolicyRemediation @CommonParams +} +elseif (Get-Command -Name New-AzPolicyRemediation -ErrorAction SilentlyContinue) { + New-AzPolicyRemediation @CommonParams +} +else { + throw "Neither Start-AzPolicyRemediation nor New-AzPolicyRemediation is available. Install/update Az.PolicyInsights." +}