Tests/ConvertTo-MsecRolePrincipalRow.Tests.ps1
|
#Requires -Module Pester # # Tests for the private role-holder row projection. Every column here is a claim about # privileged access, so they are pinned directly rather than only through # Get-MsecEntraRoleHolder's Graph mocks - the cases that matter most (an unnamed # principal, an unexpanded group) are the ones hardest to reach through the full stack. BeforeAll { $modulePath = Join-Path $PSScriptRoot '..' 'msec.psm1' Import-Module $modulePath -Force -ErrorAction Stop } AfterAll { Remove-Module msec -Force -ErrorAction SilentlyContinue } Describe 'ConvertTo-MsecRolePrincipalRow' { BeforeEach { $script:Context = @{ RoleName = 'Global Administrator' RoleTemplateId = '62e90394-69f5-4237-9190-012177145e10' IsHighlyPrivileged = $true AssignmentType = 'Active' PrincipalId = 'fallback-id' Scope = 'Tenant' DirectoryScopeId = '/' IsTenantScoped = $true EndDateTime = $null Raw = [pscustomobject]@{ id = 'assignment-1' } } } It 'reports a direct assignment with holder and assignee as the same object' { $row = InModuleScope msec -Parameters @{ Ctx = $script:Context } { param($Ctx) $user = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.user'; id = 'u1' displayName = 'Anton'; userPrincipalName = 'anton@x.com' accountEnabled = $true; userType = 'Member'; onPremisesSyncEnabled = $null } ConvertTo-MsecRolePrincipalRow -Context $Ctx -Assignee $user -Effective $user } $row.EffectiveId | Should -Be 'u1' $row.PrincipalId | Should -Be 'u1' $row.EffectiveName | Should -Be 'anton@x.com' $row.PrincipalName | Should -Be 'anton@x.com' $row.EffectiveType | Should -Be 'user' # A direct assignment is identified by MembershipType being $null and the two # ids agreeing - there is no separate is-direct column to fall out of step. $row.MembershipType | Should -BeNullOrEmpty $row.IsResolved | Should -BeTrue $row.IsActiveNow | Should -BeTrue $row.UserType | Should -Be 'Member' # Graph reports cloud-only as null; the row must say $false, not $null. $row.IsDirectorySynced | Should -BeFalse $row.PSObject.TypeNames | Should -Contain 'MsecEntraRoleHolder' } It 'keeps the assignee and drops the holder for an unexpanded group' { $row = InModuleScope msec -Parameters @{ Ctx = $script:Context } { param($Ctx) $group = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.group'; id = 'g1'; displayName = 'sg-admins' } ConvertTo-MsecRolePrincipalRow -Context $Ctx -Assignee $group -Effective $null } $row.PrincipalId | Should -Be 'g1' $row.PrincipalType | Should -Be 'group' $row.PrincipalName | Should -Be 'sg-admins' $row.EffectiveId | Should -BeNullOrEmpty $row.EffectiveName | Should -BeNullOrEmpty $row.IsResolved | Should -BeFalse # Nobody known to hold it, so "usable right now" has no answer - NOT $false, # which would assert the privilege is dormant. $row.IsActiveNow | Should -BeNullOrEmpty } It 'distinguishes an eligible holder inside an active assignment' { $row = InModuleScope msec -Parameters @{ Ctx = $script:Context } { param($Ctx) $group = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.group'; id = 'g1'; displayName = 'sg-admins' } $user = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.user'; id = 'u9'; userPrincipalName = 'pim@x.com' } ConvertTo-MsecRolePrincipalRow -Context $Ctx -Assignee $group -Effective $user -MembershipType 'Eligible' } # Both links must be active to use the role. The assignment is, the membership # is not - so the privilege is real but not usable without an activation. $row.AssignmentType | Should -Be 'Active' $row.MembershipType | Should -Be 'Eligible' $row.IsActiveNow | Should -BeFalse $row.IsResolved | Should -BeTrue $row.PrincipalType | Should -Be 'group' $row.EffectiveType | Should -Be 'user' } It 'treats a principal Graph would not name as unresolved, but keeps its id' { $out = InModuleScope msec -Parameters @{ Ctx = $script:Context } { param($Ctx) # The id-only shell: full property schema, every value null. $shell = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.user'; id = 'u404' displayName = $null; userPrincipalName = $null; accountEnabled = $null userType = $null; onPremisesSyncEnabled = $null } $stats = @{ UnreadablePrincipals = 0; UnknownHolders = 0 } $row = ConvertTo-MsecRolePrincipalRow -Context $Ctx -Assignee $shell -Effective $shell -Stats $stats [pscustomobject]@{ Row = $row; Stats = $stats } } $out.Row.IsResolved | Should -BeFalse # Never a blank cell - the id is what you would act on. $out.Row.EffectiveName | Should -Be 'u404' # Detail columns must not assert anything about an identity we cannot read. $out.Row.UserType | Should -BeNullOrEmpty $out.Row.IsDirectorySynced | Should -BeNullOrEmpty # Counted as unreadable (needs a permission grant), NOT as an unknown holder # (needs group expansion) - the two have different fixes. $out.Stats.UnreadablePrincipals | Should -Be 1 $out.Stats.UnknownHolders | Should -Be 0 } It 'counts an absent holder as an unknown holder, not an unreadable principal' { $stats = InModuleScope msec -Parameters @{ Ctx = $script:Context } { param($Ctx) $group = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.group'; id = 'g1'; displayName = 'sg' } $stats = @{ UnreadablePrincipals = 0; UnknownHolders = 0 } ConvertTo-MsecRolePrincipalRow -Context $Ctx -Assignee $group -Effective $null -Stats $stats | Out-Null $stats } $stats.UnknownHolders | Should -Be 1 $stats.UnreadablePrincipals | Should -Be 0 } It 'names a service principal by displayName and leaves user-only columns null' { $row = InModuleScope msec -Parameters @{ Ctx = $script:Context } { param($Ctx) $sp = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.servicePrincipal' id = 'sp1'; displayName = 'break-glass'; accountEnabled = $true } ConvertTo-MsecRolePrincipalRow -Context $Ctx -Assignee $sp -Effective $sp } $row.EffectiveName | Should -Be 'break-glass' $row.EffectiveType | Should -Be 'servicePrincipal' # UserPrincipalName stays strictly a UPN, so filtering on it stays exact. $row.UserPrincipalName | Should -BeNullOrEmpty $row.UserType | Should -BeNullOrEmpty $row.IsDirectorySynced | Should -BeNullOrEmpty $row.AccountEnabled | Should -BeTrue } It 'falls back to the context PrincipalId when the assignee object is absent' { $row = InModuleScope msec -Parameters @{ Ctx = $script:Context } { param($Ctx) # Graph omits the expanded principal when the app cannot read it at all. ConvertTo-MsecRolePrincipalRow -Context $Ctx -Assignee $null -Effective $null } $row.PrincipalId | Should -Be 'fallback-id' $row.PrincipalName | Should -Be 'fallback-id' $row.PrincipalType | Should -BeNullOrEmpty $row.IsResolved | Should -BeFalse } It 'carries the context''s assignment facts through unchanged' { $row = InModuleScope msec -Parameters @{ Ctx = $script:Context } { param($Ctx) $Ctx.AssignmentType = 'Eligible' $Ctx.Scope = 'AdministrativeUnit:au-1' $Ctx.DirectoryScopeId = '/administrativeUnits/au-1' $Ctx.IsTenantScoped = $false $Ctx.EndDateTime = '2026-12-31T00:00:00Z' $user = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.user'; id = 'u1'; userPrincipalName = 'a@x.com' } ConvertTo-MsecRolePrincipalRow -Context $Ctx -Assignee $user -Effective $user } $row.AssignmentType | Should -Be 'Eligible' $row.Scope | Should -Be 'AdministrativeUnit:au-1' $row.DirectoryScopeId | Should -Be '/administrativeUnits/au-1' $row.IsTenantScoped | Should -BeFalse $row.EndDateTime | Should -Be '2026-12-31T00:00:00Z' $row.RoleTemplateId | Should -Be '62e90394-69f5-4237-9190-012177145e10' $row.Raw.id | Should -Be 'assignment-1' # An eligible assignment is not usable until activated, whatever the membership. $row.IsActiveNow | Should -BeFalse } It 'rejects a MembershipType outside the two real states' { InModuleScope msec -Parameters @{ Ctx = $script:Context } { param($Ctx) $user = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.user'; id = 'u1'; userPrincipalName = 'a@x.com' } { ConvertTo-MsecRolePrincipalRow -Context $Ctx -Assignee $user -Effective $user -MembershipType 'Maybe' } | Should -Throw } } } |