<# .SYNOPSIS Set up (or remove) Rogue Productions sign-in for your organization's Microsoft Entra tenant. .DESCRIPTION The manual alternative to Organization -> Connect / Members & roles at account.rogueproductions.net. Uses Microsoft Graph PowerShell, as you, in your own tenant. Safe to re-run (idempotent) and supports -WhatIf. It never creates secrets, never adds application permissions, and changes only the "Rogue Productions" enterprise application. What it can do: * add the Rogue Productions app to your tenant and grant tenant-wide consent for openid profile email * set "assignment required" and My Apps visibility * assign users or groups to Rogue Productions roles (Owner, Admin, BillingAdmin, Member) * report current assignments and the Conditional Access policies that apply to the app * -Disconnect: remove both Rogue Productions apps from your tenant After setting up, an administrator connects the organization ONCE at https://account.rogueproductions.net/account/organization so Rogue Productions knows its Owner. .PARAMETER TenantId Your tenant id or primary domain (e.g. contoso.onmicrosoft.com). .PARAMETER AssignmentRequired $true (recommended) = only assigned users/groups can sign in. .PARAMETER VisibleInMyApps $true = show a Rogue Productions tile in My Apps for assigned people. .PARAMETER Assign Hashtable of principal (UPN, email, group name or object id) -> role, e.g. @{ 'ada@contoso.com' = 'Owner'; 'RP Users' = 'Member' } .PARAMETER AssignCsv CSV with columns Principal,Role (same meaning as -Assign). .PARAMETER Report Print assignments and applicable Conditional Access policies. .PARAMETER Disconnect Remove both Rogue Productions apps (sign-in and Organization Setup) from the tenant. .EXAMPLE ./Setup-RogueProductionsOrg.ps1 -TenantId contoso.com -AssignmentRequired $true -Assign @{ 'ada@contoso.com'='Owner'; 'Sales'='Member' } -Report .EXAMPLE ./Setup-RogueProductionsOrg.ps1 -TenantId contoso.com -Disconnect -WhatIf #> [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)][string]$TenantId, [Nullable[bool]]$AssignmentRequired = $null, [Nullable[bool]]$VisibleInMyApps = $null, [hashtable]$Assign = @{}, [string]$AssignCsv, [switch]$Report, [switch]$Disconnect ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest # Filled in by account.rogueproductions.net when you download this file. $SignInAppId = 'ebb177f4-a5ef-4ca7-9376-660fb8889018' $SetupAppId = 'c16f7142-961d-4afe-8c3d-46c42051f76e' $GraphAppId = '00000003-0000-0000-c000-000000000000' $SignInScopes = 'openid profile email' $RoleValues = @{ Owner = 'RP.Owner'; Admin = 'RP.Admin'; BillingAdmin = 'RP.BillingAdmin'; Member = 'RP.Member' } function Say([string]$m, [string]$c = 'Gray') { Write-Host " $m" -ForegroundColor $c } if ($SignInAppId -like '__*') { throw 'This copy has no app id. Download it again from https://account.rogueproductions.net/help/admins' } foreach ($m in 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Applications', 'Microsoft.Graph.Identity.SignIns', 'Microsoft.Graph.Users', 'Microsoft.Graph.Groups') { if (-not (Get-Module -ListAvailable -Name $m)) { throw "Module $m is missing. Install-Module Microsoft.Graph -Scope CurrentUser" } } $scopes = @('Application.ReadWrite.All', 'AppRoleAssignment.ReadWrite.All', 'DelegatedPermissionGrant.ReadWrite.All', 'User.Read.All', 'GroupMember.Read.All', 'Policy.Read.All') Connect-MgGraph -TenantId $TenantId -Scopes $scopes -NoWelcome | Out-Null Say "Connected to $TenantId as $((Get-MgContext).Account)" 'Cyan' function Get-Sp([string]$appId) { Get-MgServicePrincipal -Filter "appId eq '$appId'" -ErrorAction SilentlyContinue | Select-Object -First 1 } # ---------------------------------------------------------------- disconnect if ($Disconnect) { foreach ($id in @($SignInAppId, $SetupAppId)) { if ($id -like '__*') { continue } $sp = Get-Sp $id if (-not $sp) { Say "Not present: $id"; continue } if ($PSCmdlet.ShouldProcess($sp.DisplayName, 'Remove enterprise application (revokes all consent)')) { Remove-MgServicePrincipal -ServicePrincipalId $sp.Id Say "Removed $($sp.DisplayName)" 'Green' } } Say 'Also disconnect the organization at https://account.rogueproductions.net/account/organization (Danger zone).' 'Yellow' return } # ---------------------------------------------------------------- add + consent $sp = Get-Sp $SignInAppId if (-not $sp) { if ($PSCmdlet.ShouldProcess('Rogue Productions', 'Add enterprise application to tenant')) { $sp = New-MgServicePrincipal -AppId $SignInAppId Say "Added Rogue Productions ($($sp.Id))" 'Green' } } else { Say "Rogue Productions is present ($($sp.Id))" } if ($sp) { $graph = Get-Sp $GraphAppId $grant = Get-MgOauth2PermissionGrant -Filter "clientId eq '$($sp.Id)' and consentType eq 'AllPrincipals' and resourceId eq '$($graph.Id)'" -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $grant) { if ($PSCmdlet.ShouldProcess('Rogue Productions', "Grant tenant-wide consent: $SignInScopes")) { New-MgOauth2PermissionGrant -BodyParameter @{ clientId = $sp.Id; consentType = 'AllPrincipals'; resourceId = $graph.Id; scope = $SignInScopes } | Out-Null Say "Granted: $SignInScopes" 'Green' } } else { $have = @($grant.Scope -split ' ' | Where-Object { $_ }) $missing = @($SignInScopes -split ' ' | Where-Object { $have -notcontains $_ }) if ($missing.Count -and $PSCmdlet.ShouldProcess('Rogue Productions', "Add consent scopes: $($missing -join ' ')")) { Update-MgOauth2PermissionGrant -OAuth2PermissionGrantId $grant.Id -Scope (($have + $missing) -join ' ') Say "Consent updated: $(($have + $missing) -join ' ')" 'Green' } else { Say "Consent present: $($grant.Scope)" } } # ------------------------------------------------------------ access settings $patch = @{} if ($null -ne $AssignmentRequired) { $patch.AppRoleAssignmentRequired = [bool]$AssignmentRequired } if ($null -ne $VisibleInMyApps) { $tags = [System.Collections.Generic.List[string]]::new() foreach ($t in @($sp.Tags)) { if ($t -and $t -ne 'HideApp') { $tags.Add($t) } } # merge: never clobber other tags if (-not $VisibleInMyApps) { $tags.Add('HideApp') } $patch.Tags = $tags.ToArray() } if ($patch.Count -and $PSCmdlet.ShouldProcess('Rogue Productions', "Update: $($patch.Keys -join ', ')")) { Update-MgServicePrincipal -ServicePrincipalId $sp.Id -BodyParameter $patch Say "Updated: $($patch.Keys -join ', ')" 'Green' } # ------------------------------------------------------------ role assignments if ($AssignCsv) { foreach ($row in Import-Csv $AssignCsv) { $Assign[$row.Principal] = $row.Role } } if ($Assign.Count) { $sp = Get-MgServicePrincipal -ServicePrincipalId $sp.Id $existing = @(Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $sp.Id -All) foreach ($principal in $Assign.Keys) { $roleName = [string]$Assign[$principal] if (-not $RoleValues.ContainsKey($roleName)) { Say "Skip $principal - unknown role '$roleName' (Owner, Admin, BillingAdmin, Member)" 'Yellow'; continue } $role = $sp.AppRoles | Where-Object { $_.Value -eq $RoleValues[$roleName] } | Select-Object -First 1 if (-not $role) { Say "Role $roleName not found on the app in your tenant" 'Yellow'; continue } $principalId = $null if ($principal -match '^[0-9a-fA-F-]{36}$') { $principalId = $principal } if (-not $principalId) { $u = Get-MgUser -Filter "userPrincipalName eq '$principal' or mail eq '$principal'" -ErrorAction SilentlyContinue | Select-Object -First 1; if ($u) { $principalId = $u.Id } } if (-not $principalId) { $g = Get-MgGroup -Filter "displayName eq '$principal'" -ErrorAction SilentlyContinue | Select-Object -First 1; if ($g) { $principalId = $g.Id } } if (-not $principalId) { Say "Skip $principal - not found" 'Yellow'; continue } if ($existing | Where-Object { $_.PrincipalId -eq $principalId -and $_.AppRoleId -eq $role.Id }) { Say "$principal already $roleName"; continue } if ($PSCmdlet.ShouldProcess($principal, "Assign $roleName")) { New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $sp.Id -PrincipalId $principalId -ResourceId $sp.Id -AppRoleId $role.Id | Out-Null Say "Assigned $principal as $roleName" 'Green' } } } # ------------------------------------------------------------ report if ($Report) { $sp = Get-MgServicePrincipal -ServicePrincipalId $sp.Id Write-Host '' Say "Assignment required: $($sp.AppRoleAssignmentRequired) Visible in My Apps: $(-not (@($sp.Tags) -contains 'HideApp'))" 'Cyan' $byId = @{}; foreach ($r in $sp.AppRoles) { $byId[$r.Id] = $r.Value } Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $sp.Id -All | Select-Object PrincipalDisplayName, PrincipalType, @{ n = 'Role'; e = { if ($byId[$_.AppRoleId]) { $byId[$_.AppRoleId] } else { 'Default access' } } } | Format-Table -AutoSize | Out-String | Write-Host try { $ca = Get-MgIdentityConditionalAccessPolicy -All | Where-Object { $inc = $_.Conditions.Applications.IncludeApplications; $exc = $_.Conditions.Applications.ExcludeApplications (($inc -contains 'All') -or ($inc -contains $SignInAppId)) -and -not ($exc -contains $SignInAppId) } Say "Conditional Access policies that apply: $(@($ca).Count)" 'Cyan' $ca | Select-Object DisplayName, State | Format-Table -AutoSize | Out-String | Write-Host } catch { Say 'Conditional Access policies could not be read (needs Policy.Read.All).' 'Yellow' } } } Say 'Next: connect the organization once at https://account.rogueproductions.net/account/organization' 'Cyan'