diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md index 51366dac7..d0c54882e 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md @@ -5,6 +5,7 @@ - [**Objectives**](#objectives) - [**MicroHack Challenges**](#microhack-challenges) - [**Contributors**](#contributors) + # MicroHack introduction This Microsoft Sovereign Cloud MicroHack introduces engineers and architects to the core concepts, technical controls, and hands-on deployment models of Microsoft Sovereign Cloud offerings — across both Microsoft Sovereign Public Cloud and Microsoft Sovereign Private Cloud environments. @@ -14,19 +15,20 @@ This Microsoft Sovereign Cloud MicroHack introduces engineers and architects to Participants will explore how to design and operate cloud workloads that meet sovereignty, regulatory, and compliance requirements, leveraging Azure native capabilities such as Policy, RBAC, encryption, confidential compute, and hybrid enablement through Azure Arc and Azure Local. ## MicroHack context + This MicroHack scenario walks through the use of Microsoft Sovereign Cloud technologies with a focus on the best practices and the design principles and some interesting challenges for real world scenarios. Specifically, this builds up to include working with different solutions around the Microsoft Sovereign Public Cloud and the Microsoft Sovereign Private Cloud, -* [Microsoft Sovereign Cloud](https://www.microsoft.com/en-us/ai/sovereign-cloud?msockid=35d465bce58561e42620737ce487605e) -* [Microsoft Sovereign Cloud documentation](https://learn.microsoft.com/en-us/industry/sovereign-cloud/) -* [What is Sovereign Public Cloud?](https://learn.microsoft.com/en-us/industry/sovereign-cloud/sovereign-public-cloud/overview-sovereign-public-cloud) -* [Sovereign Private CLoud](https://learn.microsoft.com/en-us/industry/sovereign-cloud/sovereign-private-cloud/overview-sovereign-private-cloud) -* [Digital sovereignty](https://learn.microsoft.com/en-us/industry/sovereign-cloud/overview/digital-sovereignty) -* [Sovereign Landing Zone (SLZ)](https://learn.microsoft.com/en-us/industry/sovereign-cloud/sovereign-public-cloud/sovereign-landing-zone/overview-slz?tabs=hubspoke) -* [Azure Policy](https://learn.microsoft.com/en-us/azure/governance/policy/overview) -* [Azure encryption overview](https://learn.microsoft.com/en-us/azure/security/fundamentals/encryption-overview) -* [Azure Confidential Computing Overview](https://learn.microsoft.com/en-us/azure/confidential-computing/overview) -* [Azure Local](https://learn.microsoft.com/en-us/azure/azure-local/) -* [Azure Arc](https://learn.microsoft.com/en-us/azure/azure-arc/) +- [Microsoft Sovereign Cloud](https://www.microsoft.com/ai/sovereign-cloud?msockid=35d465bce58561e42620737ce487605e) +- [Microsoft Sovereign Cloud documentation](https://learn.microsoft.com/industry/sovereign-cloud/) +- [What is Sovereign Public Cloud?](https://learn.microsoft.com/industry/sovereign-cloud/sovereign-public-cloud/overview-sovereign-public-cloud) +- [Sovereign Private CLoud](https://learn.microsoft.com/industry/sovereign-cloud/sovereign-private-cloud/overview-sovereign-private-cloud) +- [Digital sovereignty](https://learn.microsoft.com/industry/sovereign-cloud/overview/digital-sovereignty) +- [Sovereign Landing Zone (SLZ)](https://learn.microsoft.com/industry/sovereign-cloud/sovereign-public-cloud/sovereign-landing-zone/overview-slz?tabs=hubspoke) +- [Azure Policy](https://learn.microsoft.com/azure/governance/policy/overview) +- [Azure encryption overview](https://learn.microsoft.com/azure/security/fundamentals/encryption-overview) +- [Azure Confidential Computing Overview](https://learn.microsoft.com/azure/confidential-computing/overview) +- [Azure Local](https://learn.microsoft.com/azure/azure-local/) +- [Azure Arc](https://learn.microsoft.com/azure/azure-arc/) ## Objectives @@ -53,15 +55,17 @@ This MicroHack has a few but important prerequisites In order to use the MicroHack time most effectively, the following tasks should be completed prior to starting the session. -- Your own Azure subscription with Owner RBAC rights at the subscription level -- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) (Hint: Make sure to use the lastest version) -- Contributor or Owner permissions on your subscription or resource group -- Optional: Access to Azure Arc Jumpstart LocalBox for hybrid challenges - +> [!NOTE] +> Prerequisites 1 - 3 are handled by the organizers for events hosted by Microsoft. +1. Your own Azure subscription with Owner RBAC rights at the subscription level +2. Contributor or Owner permissions on your subscription or resource group +3. Optional: Access to Azure Arc Jumpstart ArcBox & LocalBox for hybrid challenges +4. [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli). **Hint:** Make sure to use the latest version available. ## Contributors -* Thomas Maurer [GitHub](https://github.com/thomasmaurer); [LinkedIn](https://www.linkedin.com/in/thomasmaurer2/) -* Jan Egil Ring [GitHub](https://github.com/janegilring); [LinkedIn](https://www.linkedin.com/in/janegilring/) -* Murali Rao Yelamanchili [GitHub](https://github.com/yelamanchili-murali); [LinkedIn](https://www.linkedin.com/in/muraliyelamanchili/) -* Ye Zhang [GitHub](https://github.com/zhangyems); [LinkedIn](https://www.linkedin.com/in/ye-zhang-497b96a7/) + +- Thomas Maurer [GitHub](https://github.com/thomasmaurer); [LinkedIn](https://www.linkedin.com/in/thomasmaurer2/) +- Jan Egil Ring [GitHub](https://github.com/janegilring); [LinkedIn](https://www.linkedin.com/in/janegilring/) +- Murali Rao Yelamanchili [GitHub](https://github.com/yelamanchili-murali); [LinkedIn](https://www.linkedin.com/in/muraliyelamanchili/) +- Ye Zhang [GitHub](https://github.com/zhangyems); [LinkedIn](https://www.linkedin.com/in/ye-zhang-497b96a7/) diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/demo-vm-creator/deploy-arcbox.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/demo-vm-creator/deploy-arcbox.ps1 index daa9e5d7c..a5062a43b 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/demo-vm-creator/deploy-arcbox.ps1 +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/demo-vm-creator/deploy-arcbox.ps1 @@ -34,10 +34,10 @@ ArcBox flavor to deploy: ITPro, DevOps, DataOps (default: ITPro) .EXAMPLE - .\deploy-arcbox.ps1 -ResourceGroupName "rg-arcbox-shared" -Location "swedencentral" + .\deploy-arcbox.ps1 -ResourceGroupName "rg-arcbox" -Location "swedencentral" .EXAMPLE - .\deploy-arcbox.ps1 -ResourceGroupName "rg-arcbox-shared" -Location "westeurope" -DeployBastion $false + .\deploy-arcbox.ps1 -ResourceGroupName "rg-arcbox" -Location "westeurope" -DeployBastion $false .NOTES Author: MicroHack Team diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/demo-vm-creator/deploy-localbox.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/demo-vm-creator/deploy-localbox.ps1 index bc6bad760..d495de396 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/demo-vm-creator/deploy-localbox.ps1 +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/demo-vm-creator/deploy-localbox.ps1 @@ -35,7 +35,7 @@ .EXAMPLE - .\deploy-localbox.ps1 -ResourceGroupName "rg-localbox-shared" -Location "swedencentral" + .\deploy-localbox.ps1 -ResourceGroupName "rg-localbox" -Location "swedencentral" .NOTES diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/subscription-preparations/3-rbac.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/subscription-preparations/3-rbac.ps1 index c973446a6..70744b1bb 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/subscription-preparations/3-rbac.ps1 +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/subscription-preparations/3-rbac.ps1 @@ -1,22 +1,27 @@ <# .SYNOPSIS - Create a custom "Deployment Validator" RBAC role and assign permissions for MicroHack participants. + Create a custom "Deployment Validator" RBAC role and assign it to a group along with Security Reader and Resource Policy Contributor roles. .DESCRIPTION - This script is intended for MicroHack coaches who are preparing Azure subscriptions - for lab participants. It automates the setup of appropriate RBAC permissions. + This script is intended for MicroHack coaches who are preparing pre-provisioned Azure subscriptions + for lab participants. It automates the setup of appropriate RBAC permissions for lab users. - The script creates a custom Azure RBAC role that allows users to validate ARM/Bicep - deployments without granting full deployment permissions. It then assigns both this - custom role and the built-in Security Reader role to a specified Entra ID group. + The script creates a custom Azure RBAC role that allows users to validate ARM/Bicep deployments + without granting full deployment permissions. It then assigns both this custom role and the + built-in Security Reader role to a specified Entra ID group. The custom "Deployment Validator" role includes permissions to: - Validate ARM/Bicep deployments - Read deployment information - Read resource group information - This setup ensures lab participants have the necessary permissions to complete - MicroHack exercises while maintaining appropriate security boundaries. + The Security Reader role provides read-only access to security-related resources and settings. + + The Resource Policy Contributor role allows managing resource policies, including creating/modifying + policy assignments and definitions. + + This setup ensures lab participants have the necessary permissions to complete MicroHack exercises + while maintaining appropriate security boundaries in pre-provisioned subscriptions. .PARAMETER GroupName The display name of the Entra ID group to assign the roles to (default: "LabUsers") @@ -30,7 +35,7 @@ Prompts for subscription selection and uses the default group name "LabUsers" .EXAMPLE - .\3-rbac.ps1 -GroupName "SovereignCloudLabUsers" + .\3-rbac.ps1 -GroupName "LabUsers" Prompts for subscription selection and uses the specified group name .EXAMPLE @@ -51,6 +56,8 @@ https://learn.microsoft.com/azure/role-based-access-control/custom-roles .LINK https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#security-reader +.LINK + https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#resource-policy-contributor #> [CmdletBinding()] @@ -75,7 +82,7 @@ foreach ($module in $requiredModules) { Import-Module Az.Accounts, Az.Resources, Microsoft.Graph.Groups -ErrorAction Stop Write-Host "`n=== RBAC Role Assignment Utility ===" -ForegroundColor Cyan -Write-Host "This script will create a custom 'Deployment Validator' role and assign RBAC permissions." +Write-Host "This script will create a custom 'Deployment Validator' role and assign RBAC permissions." -ForegroundColor Gray Write-Host "" # Check if user is logged in to Azure @@ -100,13 +107,15 @@ function Select-AzureSubscription { if ([string]::IsNullOrWhiteSpace($SubId)) { Write-Host "`n=== Select Azure Subscription ===" -ForegroundColor Cyan + # Get all available subscriptions $subscriptions = Get-AzSubscription | Sort-Object Name if ($subscriptions.Count -eq 0) { - Write-Error "No subscriptions found." + Write-Error "No subscriptions found. Please ensure you have access to at least one subscription." exit 1 } + # Display subscriptions Write-Host "`nAvailable Subscriptions:" -ForegroundColor Yellow for ($i = 0; $i -lt $subscriptions.Count; $i++) { $sub = $subscriptions[$i] @@ -114,42 +123,59 @@ function Select-AzureSubscription { Write-Host (" {0,2}. {1} ({2}){3}" -f ($i + 1), $sub.Name, $sub.Id, $current) -ForegroundColor Gray } - $selection = Read-Host "`nEnter selection (or press Enter for current)" + Write-Host "`nOptions:" -ForegroundColor Cyan + Write-Host " - Enter a number to select a subscription" + Write-Host " - Press Enter to use the current subscription" + + $selection = Read-Host "`nYour selection" if ([string]::IsNullOrWhiteSpace($selection)) { + # Use current subscription $selectedSub = $subscriptions | Where-Object { $_.Id -eq (Get-AzContext).Subscription.Id } - if (-not $selectedSub) { $selectedSub = $subscriptions[0] } + if (-not $selectedSub) { + $selectedSub = $subscriptions[0] + } } else { - $idx = [int]$selection - 1 - if ($idx -ge 0 -and $idx -lt $subscriptions.Count) { - $selectedSub = $subscriptions[$idx] + # Parse selection + if ($selection -match '^\d+$') { + $idx = [int]$selection - 1 + if ($idx -ge 0 -and $idx -lt $subscriptions.Count) { + $selectedSub = $subscriptions[$idx] + } else { + Write-Error "Invalid selection: $selection (out of range)" + exit 1 + } } else { - Write-Error "Invalid selection" + Write-Error "Invalid selection: $selection (not a number)" exit 1 } } - Write-Host "`nSelected Subscription: $($selectedSub.Name)" -ForegroundColor Green + Write-Host "`nSelected Subscription: $($selectedSub.Name) ($($selectedSub.Id))" -ForegroundColor Green return $selectedSub.Id } else { + # Validate provided subscription ID $sub = Get-AzSubscription -SubscriptionId $SubId -ErrorAction SilentlyContinue if (-not $sub) { - Write-Error "Subscription ID '$SubId' not found." + Write-Error "Subscription ID '$SubId' not found or you don't have access to it." exit 1 } - Write-Host "`nUsing Subscription: $($sub.Name)" -ForegroundColor Green + Write-Host "`nUsing Subscription: $($sub.Name) ($($sub.Id))" -ForegroundColor Green return $SubId } } # Select subscription $subId = Select-AzureSubscription -SubId $SubscriptionId + +# Set context to selected subscription Set-AzContext -SubscriptionId $subId | Out-Null # Get group object ID Write-Host "`nLooking up Entra ID group: '$GroupName'..." -ForegroundColor Cyan try { + # Ensure we're connected to Microsoft Graph $mgContext = Get-MgContext -ErrorAction SilentlyContinue if (-not $mgContext) { Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Yellow @@ -194,38 +220,149 @@ $role = [pscustomobject]@{ AssignableScopes = @("/subscriptions/$subId") } +# Create the role definition from JSON $tmp = Join-Path $env:TEMP "deployment-validator-role.json" $role | ConvertTo-Json -Depth 10 | Set-Content -Path $tmp -Encoding UTF8 -# Check if role already exists -$existingRole = Get-AzRoleDefinition -Name "Deployment Validator" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue +# Check if role already exists (suppress warnings about delays) +Write-Verbose "Checking if role 'Deployment Validator' already exists..." -if ($existingRole) { +# Try to get all role definitions with this name (may exist in different scopes) +$allRolesWithName = Get-AzRoleDefinition -Name "Deployment Validator" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + +if ($allRolesWithName) { + # Role exists - check if it includes this subscription in assignable scopes Write-Host "Custom role 'Deployment Validator' already exists." -ForegroundColor Yellow + Write-Verbose "Role ID: $($allRolesWithName.Id)" + Write-Verbose "Assignable Scopes: $($allRolesWithName.AssignableScopes -join ', ')" + + # Check if this subscription is in the assignable scopes + if ($allRolesWithName.AssignableScopes -notcontains "/subscriptions/$subId") { + Write-Warning "The existing role does not include this subscription in its assignable scopes." + Write-Host "Current assignable scopes: $($allRolesWithName.AssignableScopes -join ', ')" -ForegroundColor Yellow + Write-Host "Attempting to update the role to include this subscription..." -ForegroundColor Cyan - if ($existingRole.AssignableScopes -notcontains "/subscriptions/$subId") { - Write-Host "Updating role to include this subscription..." -ForegroundColor Cyan try { - $existingRole.AssignableScopes = @($existingRole.AssignableScopes) + @("/subscriptions/$subId") - Set-AzRoleDefinition -Role $existingRole | Out-Null - Write-Host "Role updated successfully." -ForegroundColor Green - Start-Sleep -Seconds 10 + # Add this subscription to assignable scopes + $updatedScopes = $allRolesWithName.AssignableScopes + @("/subscriptions/$subId") + $allRolesWithName.AssignableScopes = $updatedScopes + + Set-AzRoleDefinition -Role $allRolesWithName | Out-Null + Write-Host "Successfully updated role to include this subscription." -ForegroundColor Green + + # Re-query to get updated role + Start-Sleep -Seconds 5 + $existingRole = Get-AzRoleDefinition -Name "Deployment Validator" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } catch { - Write-Warning "Could not update role: $($_.Exception.Message)" + Write-Warning "Failed to update role definition: $($_.Exception.Message)" + Write-Host "You may need to manually update the role or delete and recreate it." -ForegroundColor Yellow + $existingRole = $allRolesWithName } + } else { + Write-Host "Role is already configured for this subscription." -ForegroundColor Green + $existingRole = $allRolesWithName } } else { try { Write-Host "Creating new custom role..." -ForegroundColor Gray $newRole = New-AzRoleDefinition -InputFile $tmp -ErrorAction Stop + + # Only show success if we actually created it Write-Host "Custom role 'Deployment Validator' created successfully." -ForegroundColor Green + Write-Verbose "Role ID: $($newRole.Id)" + + # Wait a moment for role to propagate + Write-Host "Waiting for role to propagate..." -ForegroundColor Gray Start-Sleep -Seconds 10 + + # Update the existingRole variable for later use $existingRole = $newRole } catch { + # Check if it's a conflict error (role already exists) if ($_.Exception.Message -like "*Conflict*" -or $_.Exception.Message -like "*RoleDefinitionAlreadyExists*") { - Write-Host "Role already exists (detected during creation)." -ForegroundColor Yellow - Start-Sleep -Seconds 5 - $existingRole = Get-AzRoleDefinition -Name "Deployment Validator" -ErrorAction SilentlyContinue + Write-Host "Custom role 'Deployment Validator' already exists (detected during creation)." -ForegroundColor Yellow + + # Retry getting the existing role with multiple attempts (Azure replication delay) + Write-Host "Retrieving existing role definition and updating assignable scopes..." -ForegroundColor Gray + $maxRetries = 5 + $retryCount = 0 + + while (-not $existingRole -and $retryCount -lt $maxRetries) { + Start-Sleep -Seconds 3 + $retryCount++ + Write-Verbose "Attempt $retryCount of $maxRetries to retrieve role..." + + # Try method 1: Direct query by name + $existingRole = Get-AzRoleDefinition -Name "Deployment Validator" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + + if (-not $existingRole) { + # Try method 2: Search in all accessible subscriptions + Write-Verbose "Searching across all accessible subscriptions..." + $allSubscriptions = Get-AzSubscription -ErrorAction SilentlyContinue + + foreach ($sub in $allSubscriptions) { + if ($existingRole) { break } + + try { + $tempContext = Set-AzContext -SubscriptionId $sub.Id -ErrorAction SilentlyContinue + $roleInSub = Get-AzRoleDefinition -Name "Deployment Validator" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + + if ($roleInSub) { + $existingRole = $roleInSub + Write-Host "Found role in subscription: $($sub.Name)" -ForegroundColor Yellow + Write-Host "Role ID: $($existingRole.Id)" -ForegroundColor Gray + Write-Host "Current scopes: $($existingRole.AssignableScopes -join ', ')" -ForegroundColor Gray + break + } + } catch { + Write-Verbose "Could not check subscription $($sub.Name): $($_.Exception.Message)" + } + } + + # Switch back to target subscription + Set-AzContext -SubscriptionId $subId -ErrorAction SilentlyContinue | Out-Null + } + } + + if ($existingRole) { + # Check if this subscription is in assignable scopes + if ($existingRole.AssignableScopes -notcontains "/subscriptions/$subId") { + Write-Host "Updating role to include this subscription in assignable scopes..." -ForegroundColor Cyan + + try { + # Add this subscription to assignable scopes + if ($existingRole.AssignableScopes -is [System.Collections.Generic.List[string]]) { + $existingRole.AssignableScopes.Add("/subscriptions/$subId") + } else { + $existingRole.AssignableScopes = @($existingRole.AssignableScopes) + @("/subscriptions/$subId") + } + + Write-Verbose "New assignable scopes: $($existingRole.AssignableScopes -join ', ')" + + Set-AzRoleDefinition -Role $existingRole -ErrorAction Stop | Out-Null + Write-Host "Successfully updated role to include this subscription." -ForegroundColor Green + Write-Host "Waiting for updates to propagate..." -ForegroundColor Gray + Start-Sleep -Seconds 15 + + # Re-query to confirm the update + $existingRole = Get-AzRoleDefinition -Name "Deployment Validator" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + if ($existingRole) { + Write-Host "Role successfully retrieved after update." -ForegroundColor Green + Write-Verbose "Updated scopes: $($existingRole.AssignableScopes -join ', ')" + } + } catch { + Write-Warning "Could not update role: $($_.Exception.Message)" + Write-Host "You may need to manually add '/subscriptions/$subId' to the role's assignable scopes." -ForegroundColor Yellow + } + } else { + Write-Host "Successfully retrieved existing role definition." -ForegroundColor Green + Write-Verbose "Role ID: $($existingRole.Id)" + Write-Verbose "Assignable Scopes: $($existingRole.AssignableScopes -join ', ')" + } + } else { + Write-Warning "Could not retrieve the existing role definition after $maxRetries attempts." + Write-Host "Attempting to proceed with role assignment anyway..." -ForegroundColor Yellow + } } else { Write-Error "Failed to create custom role: $($_.Exception.Message)" exit 1 @@ -241,14 +378,65 @@ $existingAssignment = Get-AzRoleAssignment -ObjectId $objectId -RoleDefinitionNa if ($existingAssignment) { Write-Host "Group already has 'Deployment Validator' role assigned." -ForegroundColor Yellow } else { - try { - New-AzRoleAssignment ` - -ObjectId $objectId ` - -RoleDefinitionName "Deployment Validator" ` - -Scope "/subscriptions/$subId" | Out-Null - Write-Host "'Deployment Validator' role assigned successfully." -ForegroundColor Green - } catch { - Write-Error "Failed to assign 'Deployment Validator' role: $($_.Exception.Message)" + $assignmentSuccess = $false + $maxAssignmentRetries = 5 + $assignmentRetryCount = 0 + + while (-not $assignmentSuccess -and $assignmentRetryCount -lt $maxAssignmentRetries) { + try { + $assignmentRetryCount++ + if ($assignmentRetryCount -gt 1) { + Write-Host "Retry attempt $assignmentRetryCount of $maxAssignmentRetries..." -ForegroundColor Gray + Start-Sleep -Seconds 5 + } + + # Try to assign by name first + if ($existingRole -and $existingRole.Id) { + # If we have the role object, use the ID for more reliable assignment + Write-Verbose "Assigning role using ID: $($existingRole.Id)" + New-AzRoleAssignment ` + -ObjectId $objectId ` + -RoleDefinitionId $existingRole.Id ` + -Scope "/subscriptions/$subId" ` + -ErrorAction Stop | Out-Null + } else { + # Fall back to name-based assignment + Write-Verbose "Assigning role using name: Deployment Validator" + New-AzRoleAssignment ` + -ObjectId $objectId ` + -RoleDefinitionName "Deployment Validator" ` + -Scope "/subscriptions/$subId" ` + -ErrorAction Stop | Out-Null + } + + Write-Host "'Deployment Validator' role assigned successfully." -ForegroundColor Green + $assignmentSuccess = $true + } catch { + if ($_.Exception.Message -like "*Cannot find role definition*" -or $_.Exception.Message -like "*does not exist*") { + if ($assignmentRetryCount -lt $maxAssignmentRetries) { + Write-Host "Role definition not yet available, waiting..." -ForegroundColor Yellow + + # Try to find the role using alternative methods + if (-not $existingRole) { + Write-Verbose "Attempting to locate role definition..." + $allCustomRoles = Get-AzRoleDefinition -Custom -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $existingRole = $allCustomRoles | Where-Object { $_.Name -eq "Deployment Validator" } | Select-Object -First 1 + + if ($existingRole) { + Write-Host "Found role definition, will retry with role ID." -ForegroundColor Green + Write-Verbose "Role ID: $($existingRole.Id)" + } + } + } else { + Write-Error "Failed to assign 'Deployment Validator' role after $maxAssignmentRetries attempts: Role definition not found." + Write-Host "This may be due to the role existing in a different subscription's scope." -ForegroundColor Yellow + Write-Host "Try manually checking the role's assignable scopes in the Azure Portal under 'Access Control (IAM) > Roles'." -ForegroundColor Yellow + } + } else { + Write-Error "Failed to assign 'Deployment Validator' role: $($_.Exception.Message)" + break + } + } } } @@ -271,6 +459,25 @@ if ($existingSecurityReaderAssignment) { } } +# Assign the Resource Policy Contributor role +Write-Host "`nAssigning 'Resource Policy Contributor' role to group '$GroupName'..." -ForegroundColor Cyan + +$existingResourcePolicyContributorAssignment = Get-AzRoleAssignment -ObjectId $objectId -RoleDefinitionName "Resource Policy Contributor" -Scope "/subscriptions/$subId" -ErrorAction SilentlyContinue + +if ($existingResourcePolicyContributorAssignment) { + Write-Host "Group already has 'Resource Policy Contributor' role assigned." -ForegroundColor Yellow +} else { + try { + New-AzRoleAssignment ` + -ObjectId $objectId ` + -RoleDefinitionName "Resource Policy Contributor" ` + -Scope "/subscriptions/$subId" | Out-Null + Write-Host "'Resource Policy Contributor' role assigned successfully." -ForegroundColor Green + } catch { + Write-Error "Failed to assign 'Resource Policy Contributor' role: $($_.Exception.Message)" + } +} + # Display summary Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan Write-Host "SUMMARY" -ForegroundColor Cyan @@ -283,5 +490,6 @@ Write-Host "" Write-Host "Roles Assigned:" -ForegroundColor Yellow Write-Host " 1. Deployment Validator (Custom)" -ForegroundColor Green Write-Host " 2. Security Reader (Built-in)" -ForegroundColor Green +Write-Host " 3. Resource Policy Contributor (Built-in)" -ForegroundColor Green Write-Host "" Write-Host "Configuration complete!" -ForegroundColor Green diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/subscription-preparations/4-resource-groups.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/subscription-preparations/4-resource-groups.ps1 new file mode 100644 index 000000000..65234b7ef --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/subscription-preparations/4-resource-groups.ps1 @@ -0,0 +1,155 @@ +<# +.SYNOPSIS + Create resource groups and assign Owner role to MicroHack participants. + +.DESCRIPTION + This script is intended for MicroHack coaches who are preparing Azure subscriptions + for lab participants. It automates the creation of resource groups and RBAC role + assignments for each lab user. + + The script creates numbered resource groups (e.g., labuser-01, labuser-02, etc.) + and assigns the Owner role to the corresponding user for their resource group. + + This setup ensures each lab participant has their own isolated resource group + with full control to complete MicroHack exercises. + +.PARAMETER SubscriptionName + The name of the Azure subscription where resource groups should be created. + Default: "Micro-Hack-1" + +.PARAMETER Location + The Azure region where resource groups should be created. + Default: "northeurope" + +.PARAMETER ResourceGroupPrefix + The prefix for resource group names. + Default: "labuser-" + +.PARAMETER ResourceGroupCount + The number of resource groups to create. Should match the number of users + created in Create MH Users.ps1. + Default: 60 + +.PARAMETER StartIndex + The starting index for resource group numbering. + Default: 0 + +.EXAMPLE + .\4-resource-groups.ps1 + Creates resource groups using default values + +.NOTES + Author: MicroHack Team + Date: February 2026 + + Prerequisites: + - Az.Accounts module + - Az.Resources module + - Appropriate permissions to create resource groups and assign RBAC roles + - Users must be created first (see Create MH Users.ps1) + +.LINK + https://learn.microsoft.com/azure/azure-resource-manager/management/manage-resource-groups-powershell +.LINK + https://learn.microsoft.com/azure/role-based-access-control/role-assignments-powershell +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [string]$SubscriptionName = "Micro-Hack-1", + + [Parameter(Mandatory = $false)] + [string]$Location = "northeurope", + + [Parameter(Mandatory = $false)] + [string]$ResourceGroupPrefix = "labuser-", + + [Parameter(Mandatory = $false)] + [int]$ResourceGroupCount = 60, + + [Parameter(Mandatory = $false)] + [int]$StartIndex = 0 +) + +# Ensure required modules are available +$requiredModules = @('Az.Accounts', 'Az.Resources') +foreach ($module in $requiredModules) { + if (-not (Get-Module -ListAvailable -Name $module)) { + Write-Error "$module module is not installed. Please run: Install-Module -Name $module" + exit 1 + } +} + +# Import required modules +Import-Module Az.Accounts, Az.Resources -ErrorAction Stop + +Write-Host "`n=== Resource Group Creation Utility ===" -ForegroundColor Cyan +Write-Host "This script will create resource groups and assign Owner role to lab users." +Write-Host "" + +# Check if user is logged in to Azure +try { + $context = Get-AzContext + if (-not $context) { + Write-Host "No Azure context found. Please login..." -ForegroundColor Yellow + Connect-AzAccount -UseDeviceAuthentication + $context = Get-AzContext + } else { + Write-Host "Using Azure account: $($context.Account.Id)" -ForegroundColor Green + } +} catch { + Write-Error "Failed to get Azure context. Please run Connect-AzAccount first." + exit 1 +} + +# Set subscription context +try { + Set-AzContext -Subscription $SubscriptionName | Out-Null + Write-Host "Using subscription: $SubscriptionName" -ForegroundColor Green +} catch { + Write-Error "Failed to set subscription context: $($_.Exception.Message)" + exit 1 +} + +$UPNSuffix = '@' + ((Get-AzContext).Account.Id -split "@")[1] # Get UPN suffix from the signed-in account (@xxx.onmicrosoft.com) +$UPNSuffix = '@micro-hack.arcmasterclass.cloud' + +Write-Host "`nStarting resource group creation..." -ForegroundColor Cyan +Write-Host " Prefix: $ResourceGroupPrefix" -ForegroundColor Gray +Write-Host " Count: $ResourceGroupCount" -ForegroundColor Gray +Write-Host " Location: $Location" -ForegroundColor Gray +Write-Host "" + +for ($i = 1; $i -le $ResourceGroupCount; $i++) { + + $ResourceGroupNumber = $StartIndex+$i + $ResourceGroupName = "$ResourceGroupPrefix$ResourceGroupNumber" + $ResourceGroupName = "$ResourceGroupPrefix{0:D2}" -f $ResourceGroupNumber + + try { + $null = New-AzResourceGroup -Name $ResourceGroupName -Location $Location + Write-Host "Resource group $ResourceGroupName has been created" -ForegroundColor Green + } + catch { + Write-Host "Failed to create resource group $ResourceGroupName -ForegroundColor Red + } + + Write-Host "Updating role assignments for resource group $ResourceGroupName" -ForegroundColor Cyan + + # Assign Owner role to the user for their respective resource group + # Note: This requires the users to be created first, and may need to be adjusted based on how users are created and identified in your environment + + $SignInName = $ResourceGroupName + $UPNSuffix + + try { + $null = New-AzRoleAssignment -SignInName $SignInName -ResourceGroupName $ResourceGroupName -RoleDefinitionName 'Owner' + Write-Host "Role assignment completed for user $SignInName in resource group $ResourceGroupName" -ForegroundColor Green + } + catch { + Write-Host "Failed to assign role to user $SignInName in resource group $ResourceGroupName -ForegroundColor Red + } + + +} +Write-Host "`nResource group creation and role assignment process completed." -ForegroundColor Cyan \ No newline at end of file