Public/New-IntuneCloneAppRegistration.ps1
|
# Auto-generated by module/Build-Module.ps1 from en/tools/New-IntuneCloneKitAppRegistration.ps1 # Edit the source script, then re-run Build-Module.ps1. Do not edit this file directly. function New-IntuneCloneAppRegistration { <# .SYNOPSIS Prepare, IN A SINGLE STEP, the app-only identity (app registration + certificate + admin consent) required for the ZERO-TOUCH execution of the kit in a tenant. .DESCRIPTION To be run once per tenant, signed in as an administrator (Global Admin or Privileged Role Admin + Application Admin). This is the ONLY interactive step of the whole chain: the orchestrator then runs without any intervention. The script: 1. connects to Graph in delegated mode (Application.ReadWrite.All, AppRoleAssignment.ReadWrite.All); 2. creates (or reuses) a self-signed certificate in Cert:\CurrentUser\My; 3. creates the app registration with the certificate as credential; 4. adds the APPLICATION Microsoft Graph permissions suited to the role (SOURCE = read, TARGET = write); 5. creates the service principal and grants ADMIN CONSENT (appRoleAssignments); 6. displays the ClientId + the thumbprint to paste into config.ps1. .PARAMETER TenantId GUID of the tenant to prepare. .PARAMETER Role Source (READ permissions) or Target (WRITE permissions). .PARAMETER DisplayName Name of the app registration. Default: IntuneCloneKit-<Role>. .PARAMETER CertThumbprint Reuse an existing certificate (thumbprint) instead of creating one. .PARAMETER CertYears Validity period of the created certificate (default 2 years). .PARAMETER SkipConsent Do not attempt admin consent via script (to be done afterwards in the portal). .EXAMPLE .\New-IntuneCloneKitAppRegistration.ps1 -TenantId <SRC> -Role Source .\New-IntuneCloneKitAppRegistration.ps1 -TenantId <TGT> -Role Target #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$TenantId, [Parameter(Mandatory)][ValidateSet('Source','Target')][string]$Role, [string]$DisplayName, [string]$CertThumbprint, [int]$CertYears = 2, [switch]$SkipConsent ) $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $GraphAppId = '00000003-0000-0000-c000-000000000000' # Microsoft Graph if (-not $DisplayName) { $DisplayName = "IntuneCloneKit-$Role" } # Application permissions (app roles) required, per role. $PermsSource = @( 'DeviceManagementConfiguration.Read.All','DeviceManagementApps.Read.All', 'DeviceManagementServiceConfig.Read.All','DeviceManagementRBAC.Read.All', 'DeviceManagementManagedDevices.Read.All','Group.Read.All','Organization.Read.All','Policy.Read.All' ) $PermsTarget = @( 'DeviceManagementConfiguration.ReadWrite.All','DeviceManagementApps.ReadWrite.All', 'DeviceManagementServiceConfig.ReadWrite.All','DeviceManagementRBAC.ReadWrite.All', 'DeviceManagementManagedDevices.ReadWrite.All','Group.ReadWrite.All','Organization.Read.All','Policy.ReadWrite.ConditionalAccess' ) $Perms = if ($Role -eq 'Source') { $PermsSource } else { $PermsTarget } function Info { param($T) Write-Host ("[INFO] {0}" -f $T) -ForegroundColor Cyan } function Ok { param($T) Write-Host ("[OK] {0}" -f $T) -ForegroundColor Green } function Warn { param($T) Write-Host ("[WARN] {0}" -f $T) -ForegroundColor Yellow } Write-Host '' Write-Host ('=' * 84) -ForegroundColor DarkGray Write-Host ("APP-ONLY PREPARATION - Tenant {0} - Role {1}" -f $TenantId, $Role) -ForegroundColor Green Write-Host ('=' * 84) -ForegroundColor DarkGray # --- Module --- if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication)) { Info 'Installing Microsoft.Graph.Authentication (CurrentUser).' Install-Module Microsoft.Graph.Authentication -Scope CurrentUser -Force -AllowClobber } Import-Module Microsoft.Graph.Authentication -Force # --- Admin connection (interactive, one-time) --- Info 'Administrator connection to the tenant (a sign-in window may open)...' Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null Connect-MgGraph -TenantId $TenantId -Scopes 'Application.ReadWrite.All','AppRoleAssignment.ReadWrite.All','Directory.Read.All' -NoWelcome $ctx = Get-MgContext if (-not $ctx -or $ctx.TenantId.ToLowerInvariant() -ne $TenantId.ToLowerInvariant()) { throw "Connected to the wrong tenant. Expected $TenantId, connected $($ctx.TenantId)" } Ok ("Connected: {0} (Tenant {1})" -f $ctx.Account, $ctx.TenantId) # --- Microsoft Graph service principal + app role resolution --- Info 'Resolving Microsoft Graph permission identifiers...' $graphSp = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/servicePrincipals(appId='$GraphAppId')" $roleByValue = @{} foreach ($r in $graphSp.appRoles) { if ($r.allowedMemberTypes -contains 'Application') { $roleByValue[$r.value] = $r.id } } $resourceAccess = @() $resolved = @() foreach ($p in $Perms) { if ($roleByValue.ContainsKey($p)) { $resourceAccess += @{ id = $roleByValue[$p]; type = 'Role' }; $resolved += $p } else { Warn ("Permission not found in this tenant: {0} (ignored)" -f $p) } } Ok ("{0} application permission(s) resolved." -f $resourceAccess.Count) # --- Certificate --- if ($CertThumbprint) { $cert = Get-Item ("Cert:\CurrentUser\My\{0}" -f $CertThumbprint) -ErrorAction SilentlyContinue if (-not $cert) { throw "Certificate $CertThumbprint not found in Cert:\CurrentUser\My" } Ok ("Certificate reused: {0}" -f $cert.Thumbprint) } else { $subject = "CN=IntuneCloneKit-$Role" Info ("Creating a self-signed certificate {0} (valid {1} year(s))..." -f $subject, $CertYears) $cert = New-SelfSignedCertificate -Subject $subject -CertStoreLocation 'Cert:\CurrentUser\My' ` -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 ` -HashAlgorithm SHA256 -NotAfter (Get-Date).AddYears($CertYears) Ok ("Certificate created: {0}" -f $cert.Thumbprint) } $certB64 = [Convert]::ToBase64String($cert.GetRawCertData()) # --- App registration (creation or reuse by displayName) --- $existing = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/applications?`$filter=displayName eq '$DisplayName'").value if ($existing -and $existing.Count -gt 0) { $app = $existing[0] Warn ("App '{0}' already exists (AppId {1}): updating permissions + adding the certificate." -f $DisplayName, $app.appId) $patch = @{ requiredResourceAccess = @(@{ resourceAppId = $GraphAppId; resourceAccess = $resourceAccess }) keyCredentials = @(@{ type='AsymmetricX509Cert'; usage='Verify'; key=$certB64; displayName=$cert.Subject }) } Invoke-MgGraphRequest -Method PATCH -Uri "https://graph.microsoft.com/v1.0/applications/$($app.id)" -Body ($patch | ConvertTo-Json -Depth 10) -ContentType 'application/json' | Out-Null $app = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/applications/$($app.id)" } else { Info ("Creating the app registration '{0}'..." -f $DisplayName) $body = @{ displayName = $DisplayName signInAudience = 'AzureADMyOrg' requiredResourceAccess = @(@{ resourceAppId = $GraphAppId; resourceAccess = $resourceAccess }) keyCredentials = @(@{ type='AsymmetricX509Cert'; usage='Verify'; key=$certB64; displayName=$cert.Subject }) } $app = Invoke-MgGraphRequest -Method POST -Uri 'https://graph.microsoft.com/v1.0/applications' -Body ($body | ConvertTo-Json -Depth 10) -ContentType 'application/json' Ok ("App created: AppId {0}" -f $app.appId) } # --- App service principal --- $sp = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId eq '$($app.appId)'").value | Select-Object -First 1 if (-not $sp) { Info 'Creating the service principal...' $sp = Invoke-MgGraphRequest -Method POST -Uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -Body (@{ appId = $app.appId } | ConvertTo-Json) -ContentType 'application/json' } Ok ("Service principal: {0}" -f $sp.id) # --- Admin consent (appRoleAssignments) --- if ($SkipConsent) { Warn 'Admin consent not attempted (-SkipConsent). To grant in the portal: Azure AD > App registrations > API permissions > Grant admin consent.' } else { Info 'Granting admin consent (may require a few seconds of replication)...' $existingAssign = @() try { $existingAssign = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($sp.id)/appRoleAssignments").value } catch {} $already = @($existingAssign | ForEach-Object { $_.appRoleId }) $granted = 0; $failed = @() foreach ($ra in $resourceAccess) { if ($already -contains $ra.id) { $granted++; continue } $assign = @{ principalId = $sp.id; resourceId = $graphSp.id; appRoleId = $ra.id } $done = $false for ($try = 1; $try -le 5 -and -not $done; $try++) { try { Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($sp.id)/appRoleAssignments" -Body ($assign | ConvertTo-Json) -ContentType 'application/json' | Out-Null $done = $true; $granted++ } catch { if ($try -lt 5) { Start-Sleep -Seconds 5 } else { $failed += $ra.id } } } } if ($failed.Count -eq 0) { Ok ("Admin consent granted ({0} permission(s))." -f $granted) } else { Warn ("{0} permission(s) granted, {1} failed. Finish in the portal via 'Grant admin consent'." -f $granted, $failed.Count) } } # --- Output --- Write-Host '' Write-Host ('-' * 84) -ForegroundColor DarkGray Write-Host 'TO PASTE INTO config.ps1:' -ForegroundColor Green if ($Role -eq 'Source') { Write-Host ("`$SourceClientId = '{0}'" -f $app.appId) -ForegroundColor White Write-Host ("`$SourceCertThumbprint = '{0}'" -f $cert.Thumbprint) -ForegroundColor White } else { Write-Host ("`$TargetClientId = '{0}'" -f $app.appId) -ForegroundColor White Write-Host ("`$TargetCertThumbprint = '{0}'" -f $cert.Thumbprint) -ForegroundColor White } Write-Host ('-' * 84) -ForegroundColor DarkGray Write-Host '' Info 'Reminder: the certificate must remain in the store of the account/machine that will launch the orchestrator.' Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null } |