en-US/about_Omnicit.EntraRBAC.help.txt

TOPIC
    about_Omnicit.EntraRBAC

SHORT DESCRIPTION
    Manage Entra ID and Azure RBAC building blocks across tenants.

LONG DESCRIPTION
    Omnicit.EntraRBAC manages Entra ID groups and PIM, Administrative Units,
    Entitlement Management, Access Reviews, Azure resources and RBAC, and Azure PIM
    across multiple tenants, plus a JSON inventory and a declarative apply engine.
    Authenticate with Connect-OER, then use the OER cmdlets. Configuration per tenant
    is stored in Tenant Profile files managed by New/Get/Set/Remove-OERConfiguration.

    Calling Connect-OER is optional. Every cmdlet that reaches Graph or Azure
    authenticates on first use, reusing a cached token for the same tenant and identity.
    Cmdlets that reach Azure Resource Manager need an ARM token; pass -IncludeARM to
    Connect-OER to acquire one up front.

    All state-changing cmdlets support -WhatIf and -Confirm. Deletions of high-value
    objects use ConfirmImpact High and warn before the destructive call.

    Use Get-Command -Module Omnicit.EntraRBAC to list all 98 cmdlets, and
    Get-Help <cmdlet> -Full for parameters and examples. Use Get-OERRequiredScope for
    the permissions any of them needs; see PERMISSIONS below.

COMMAND COHORTS

    The cohorts below are for orientation only. They are NOT permission boundaries, so
    none of them carries a scope line -- ask Get-OERRequiredScope instead.

    Authentication and Tenant Profiles
        Connect-OER, Disconnect-OER
        New-OERConfiguration, Get-OERConfiguration, Set-OERConfiguration,
        Remove-OERConfiguration

    Groups and PIM for Groups
        New-OERGroup, Get-OERGroup, Set-OERGroup, Remove-OERGroup
        Add-OERGroupMember, Get-OERGroupMember, Remove-OERGroupMember
        Add-OERGroupEligibility, Get-OERGroupEligibility, Remove-OERGroupEligibility
        Get-OERGroupPimPolicy, Set-OERGroupPimPolicy

    Administrative Units
        New-OERAdministrativeUnit, Get-OERAdministrativeUnit,
        Set-OERAdministrativeUnit, Remove-OERAdministrativeUnit
        Add-OERAdministrativeUnitMember, Remove-OERAdministrativeUnitMember
        Add-OERAdministrativeUnitScopedRole, Get-OERAdministrativeUnitScopedRole,
        Remove-OERAdministrativeUnitScopedRole
        New-OERGroup -AdministrativeUnit places a new group into an AU at creation time.

    Entitlement Management
        Catalogs: New-OERCatalog, Get-OERCatalog, Set-OERCatalog,
                           Remove-OERCatalog
        Catalog resources: Add-OERCatalogResource, Get-OERCatalogResource,
                           Remove-OERCatalogResource
        Access packages: New-OERAccessPackage, Get-OERAccessPackage,
                           Set-OERAccessPackage, Remove-OERAccessPackage
        Resource roles: Add-OERAccessPackageResourceRole,
                           Get-OERAccessPackageResourceRole,
                           Remove-OERAccessPackageResourceRole
        Policy builders: New-OERAccessPackageApprovalStage,
                           New-OERAccessPackageRequestorScope,
                           New-OERAccessPackageRequestorSettings
        Policies: New-OERAccessPackageAssignmentPolicy,
                           Get-OERAccessPackageAssignmentPolicy,
                           Set-OERAccessPackageAssignmentPolicy,
                           Remove-OERAccessPackageAssignmentPolicy
        Assignments: New-OERAccessPackageAssignment,
                           Get-OERAccessPackageAssignment,
                           Remove-OERAccessPackageAssignment

    Access Reviews
        New-OERAccessReviewDefinition, Get-OERAccessReviewDefinition,
        Set-OERAccessReviewDefinition, Remove-OERAccessReviewDefinition
        New-OERAccessReviewStage composes one stage of a multi-stage review.
        Get-OERAccessReviewInstance, Get-OERAccessReviewInstanceDecision,
        Stop-OERAccessReviewInstance, Invoke-OERAccessReviewInstanceDecision,
        Send-OERAccessReviewReminder

    Directory roles
        Get-OERDirectoryRoleManagementPolicy, Set-OERDirectoryRoleManagementPolicy
        Eligible: New-OEREligibleDirectoryRoleAssignment,
                   Get-OEREligibleDirectoryRoleAssignment,
                   Remove-OEREligibleDirectoryRoleAssignment
        Active: New-OERActiveDirectoryRoleAssignment,
                   Get-OERActiveDirectoryRoleAssignment,
                   Remove-OERActiveDirectoryRoleAssignment

    Azure inventory, RBAC, resource groups and resources
        Get-OERManagementGroup, Get-OERSubscription, Get-OERRoleDefinition
        Get-OERRoleAssignment, New-OERRoleAssignment, Set-OERRoleAssignment,
        Remove-OERRoleAssignment
        New-OERResourceGroup, Get-OERResourceGroup, Set-OERResourceGroup,
        Remove-OERResourceGroup, Get-OERResource

    Azure PIM
        Eligibility: New-OEREligibleRoleAssignment, Get-OEREligibleRoleAssignment,
                      Remove-OEREligibleRoleAssignment
        Active: New-OERActiveRoleAssignment, Get-OERActiveRoleAssignment,
                      Remove-OERActiveRoleAssignment
        Activation: Enable-OEREligibleRoleAssignment, Disable-OEREligibleRoleAssignment
        Policy: Get-OERRoleManagementPolicy, Set-OERRoleManagementPolicy,
                      New-OERPolicyNotificationRule

    Inventory and JSON orchestration
        Get-OERInventory reads tenant state into a round-trippable inventory object.
        Export-OERInventory writes that state to a bundle folder (JSON, schema, LLM
        prompt, README). Test-OERStructure validates a structure document offline with
        no tenant call. Invoke-OERStructure applies one idempotently in dependency
        order, with -WhatIf plan mode and opt-in child-scope -Prune.

    Permissions
        Get-OERRequiredScope reports the Graph permissions and Azure RBAC roles one or
        more cmdlets need. See PERMISSIONS below.

    Conditional Access
        Get-OERAuthenticationContext

SYNCHRONIZED GROUPS

    Get-OERGroup shows OnPremisesSyncEnabled for every group: True for a group
    synchronized from on-premises Active Directory, which is managed there and
    read-only in the cloud. Invoke-OERStructure writes nothing to such a group:
    each change to its properties, members, owners, eligibility or pimPolicy is
    reported Skipped, with one warning per group, and -Prune removes nothing from
    it. Export-OERInventory marks it onPremisesSynced in groupsRoster.json, and
    -IncludeSyncedGroups keeps the synchronized security groups in full detail in
    inventory.json.

PERMISSIONS

    Ask the module rather than reading a table kept in step by hand:

        Get-OERRequiredScope -Cmdlet New-OERGroup
        Get-OERRequiredScope -Cmdlet Get-OERGroup* -Unique
        Get-OERRequiredScope -Unique

    It reads a static table, so it needs no tenant and no sign-in and answers before
    Connect-OER. Three things worth knowing about the answers:

    - The area a cmdlet belongs to is not its permission boundary. Microsoft Graph
      grants permissions by the resource a call touches, so one scope line covering a
      whole cohort is wrong by construction. The three *AdministrativeUnitScopedRole
      cmdlets write directory role memberships rather than unit memberships and need a
      role-management write permission; Get-OERGroupPimPolicy and Set-OERGroupPimPolicy
      hit roleManagementPolicies and need the RoleManagementPolicy permissions, not the
      eligibility permissions the other PIM-for-Groups cmdlets use.

    - Each answer is transitive. It covers the cmdlet's own calls plus every call its
      internal helpers make, including the directory reads behind friendly-name
      parameters such as -User, -Group and -ServicePrincipal. Consenting to what is
      listed is enough for every parameter form; the Note property says so where a
      permission is needed for only one of them.

    - The Transport property disambiguates an empty list: Graph, Arm, GraphAndArm, or
      None for a cmdlet that touches no tenant at all. An empty GraphScope on an
      ARM-only cmdlet means "none needed", never "unknown".

    Where an entry lists User.ReadBasic.All, Group.Read.All and Application.Read.All, a
    single Directory.Read.All covers all three; the narrower permissions are listed
    because the module prefers least privilege. Where an entry lists Directory.Read.All
    instead, that cmdlet reads directoryObjects, which accepts nothing narrower, so the
    covered permissions are deliberately NOT also listed and the Note property says
    which parameter makes the call. Azure cmdlets additionally need an ARM token: pass
    -IncludeARM to Connect-OER, or let the cmdlet acquire one.

TENANT PROFILE

    A Tenant Profile is a PSD1 file holding per-tenant configuration, stored under
    <home>/.config/Omnicit.EntraRBAC/Profiles/<alias>.psd1 and managed with
    New-OERConfiguration, Get-OERConfiguration, Set-OERConfiguration and
    Remove-OERConfiguration. <home> is the current user's profile folder as .NET
    reports it, falling back to $HOME: the same directory as $env:USERPROFILE on
    Windows, and the user's home directory on Linux and macOS. Connect-OER
    -TenantAlias <alias> resolves the TenantId from it. Only TenantId is required;
    every other section is optional.

        @{
            TenantId = '00000000-0000-0000-0000-000000000000'
            Naming = @{
                Group = 'role_sec_{area}_{tier}'
                AU = '{prefix}_au_{name}'
                Catalog = 'CAT-{org}-{scope}'
            }
            Defaults = @{
                PrimaryApprovers = @('<guid>', '<guid>')
                EscalationApprovers = @('<guid>')
                Catalog = 'CAT-IT-PRG-Core'
                AuthenticationContextId = 'c1'
                ActivationMaxHours = 8
            }
        }

    Naming templates substitute {token} placeholders case-insensitively. An unresolved
    placeholder is an error, so a malformed name is never produced. Set-OERConfiguration
    preserves sections you do not supply; New-OERConfiguration is create-only and errors
    if the alias already exists.

    A profile may also carry an optional Environment key naming the sovereign cloud that
    tenant lives in: Global, USGov, USGovDoD or China. See SOVEREIGN CLOUDS below.

LONG RUNS

    An access token lasts about an hour: Microsoft Entra ID gives one a
    default lifetime of 60 to 90 minutes, so a run longer than that outlives
    the token it started with. Where the sign-in type allows it, the module
    renews a token in two places: when a command starts within five minutes
    of the token's expiry, and in the middle of a command when Microsoft Graph
    or Azure Resource Manager rejects the token with 401, after which the
    rejected request is sent once more. What a renewal costs depends on the
    sign-in type:

        Interactive Every renewal opens the browser to sign in
                            again, so a long run asks you to sign in about
                            once an hour. The account picker lets you
                            choose another account of the same tenant, and
                            the command then carries on as that account:
                            choose the account the session signed in with
        Device code Every renewal prints a new code to enter, about
                            once an hour, as every device code sign-in does:
                            see the known limitation under SWITCHING TENANTS
        Managed identity Renews with no prompt
        Client secret Not renewed, since the module never keeps the
        and certificate secret or certificate: once the token has
                            expired a request is refused with
                            AppOnlyTokenRefreshUnsatisfiable, and a command
                            that starts within five minutes of its expiry,
                            or later, fails to sign in with
                            AppOnlySessionCredentialUnavailable, until you
                            run Connect-OER with the secret or certificate
                            again

    Before a long run, run Connect-OER -Force with the session's own
    sign-in (the same -TenantId, credential and -IncludeARM as the first
    Connect-OER), so the run starts with a new token. A plain Connect-OER
    with the same sign-in returns the session it has while its token has more
    than five minutes left, and a bare Connect-OER -Force signs in
    interactively, as a bare Connect-OER always does. Stay at the keyboard
    during an interactive or device code run: a managed identity is the only
    sign-in that renews unattended.

    An app-only run renews only between commands. Run Connect-OER -Force
    with the secret or certificate before each command, and keep each command
    shorter than a token's lifetime. Without -Force, Connect-OER with the
    same credential returns at once while the token has more than five
    minutes left and signs in again when it has less.

    A renewal nobody completes (a browser sign-in or a device code left
    unanswered) fails like any other sign-in: the request it was for is not
    sent, and the session is left uncertain, so a later command that names no
    tenant sends nothing, as GRAPH SDK SESSION below describes.

SWITCHING TENANTS

    One PowerShell session works in one tenant at a time. Whether a later
    Connect-OER call naming a different tenant actually switches depends on
    the sign-in type:

        Client secret Reuses the credential built for the earlier tenant, so the
                            switching sign-in fails by default -- or, with
                            AZURE_IDENTITY_DISABLE_MULTITENANTAUTH set, requests its
                            token from the earlier tenant, and the module refuses that
                            token (TenantMismatch) -- until you run Connect-OER ...
                            -Force
        Device code Does not send the tenant you name; its token may
                            come from the signed-in account's own tenant when
                            that account cannot obtain one in the tenant you
                            name, and the module then refuses it
                            (TenantMismatch) -- and every device code sign-in
                            asks for a new code: see the known limitation
                            below
        Managed identity Does not send the tenant you name; its token
                            normally comes from the identity's own tenant, and
                            the module refuses it when that is not the tenant
                            you name (TenantMismatch)
        Interactive Signs in to the tenant you name
        Certificate Signs in to the tenant you name

    KNOWN LIMITATION -- every device code sign-in asks for a new code. The
    module makes AzAuth build a new credential for every device code sign-in,
    Microsoft Graph and Azure Resource Manager alike, so each one prints a new
    code to enter -- two with -IncludeARM, one for each token. That covers the
    first sign-in in a process, one naming another tenant, a new Connect-OER
    after Disconnect-OER, and every renewal of the token: a command that starts
    within five minutes of its expiry, a request whose token Microsoft Graph or
    Azure Resource Manager rejects, and a claims challenge. Before this release
    such a sign-in could reuse the credential AzAuth keeps for the PowerShell
    process and never return, with no code printed and no error; Connect-OER
    -Force is no longer needed to avoid that. A command that needs only tokens
    the session already holds, each with more than five minutes left, asks for
    no code. Each code has to be entered, so a long device code run needs
    someone at the keyboard.

    The module checks each token against the tenant you name, whether you
    named it by its tenant ID or by a domain, and refuses a token issued for
    another tenant with TenantMismatch before it is used. For a domain it
    first looks the tenant ID up at the cloud's Microsoft Entra ID authority:
    one unauthenticated request per domain and cloud, remembered for as long
    as the module stays imported, and asked again at the next sign-in if it
    fails. A domain the authority does not resolve to a tenant is refused
    with TenantResolutionFailed, and no token is requested. A tenant is a
    GUID or a verified domain: any other value is looked up the same way and
    refused when it names no tenant, 'common' among them. 'organizations',
    which the module uses when no tenant is named and there is no session,
    names no tenant and is not checked against one. When a sign-in under it
    acquires an Azure Resource Manager token, that token must come from the
    same tenant as the session's Microsoft Graph token, or it is refused with
    TenantMismatch. When only the Microsoft Graph token is renewed, the
    session keeps its Azure Resource Manager token only if the renewed
    Microsoft Graph token comes from the same tenant; otherwise it drops it
    and, as soon as a command needs Azure Resource Manager, acquires a new
    one, checked the same way.

    The module also warns before a client secret sign-in for the same
    application when the tenant you name differs from the one the credential
    AzAuth is currently holding for that application was built for, and no
    Force is on the call: you did not pass -Force, and the module did not add
    Force itself for a sovereign-cloud switch. AzAuth only rebuilds that
    credential for -Force, for a different application, or for a different
    kind of sign-in -- a plain retry of the same switch, with none of those,
    reuses the same credential and keeps warning every time, whether the
    earlier attempt's token came from the old tenant or was refused outright.
    This is the module's own view: a Get-AzToken call made outside the module
    does not update it, and it resets only when Omnicit.EntraRBAC itself is
    re-imported.

    The warning does not stop the sign-in, but under -WarningAction Stop (or
    $WarningPreference = 'Stop') it does, before any token is requested --
    the same safe direction as the module's existing ambient
    AZURE_AUTHORITY_HOST warning.

    Disconnect-OER clears this module's own session state and closes the
    Graph SDK session the module connected; it does not clear the credential
    AzAuth keeps for the process. Connect-OER -Force is the supported way,
    inside the same PowerShell process, to move a client secret sign-in to a
    new tenant.

    Name a tenant by its tenant ID (a GUID) or by a verified domain -- a
    Tenant Profile's TenantId included. Both are checked, and a token issued
    for another tenant is refused either way; a GUID saves the lookup.

        # Move a client secret sign-in for the same application to
        # another tenant
        Connect-OER -TenantId '00000000-0000-0000-0000-000000000000' `
            -ClientId $AppId -ClientSecret $Secret -Force

GRAPH SDK SESSION

    Connect-OER sets up a Microsoft Graph PowerShell SDK session in the current
    process: it calls Connect-MgGraph with the module's token, and so does the
    automatic sign-in of any other OER cmdlet. Disconnect-OER closes only the
    session the module connected. A session it did not connect, or has no
    record of connecting -- one another Connect-MgGraph started, say -- is left
    connected, with a warning written before the confirmation, so -WhatIf shows
    it. Run Disconnect-MgGraph to end that one; the next OER cmdlet signs in
    again, and its own Connect-MgGraph replaces it, as any Connect-MgGraph
    does.

    If another Connect-MgGraph -- yours, or another tool's -- replaces the
    module's session in the same process, the next OER cmdlet sends nothing: it
    refuses its Microsoft Graph calls with GraphSessionChanged instead of
    sending them under that session, and its Azure Resource Manager calls with
    SignInRefused. An error can be reported more than once for one cmdlet. The
    module never switches the session back by itself. Run Connect-OER with
    the same sign-in the session used -- for an app-only session, its
    certificate or client secret, since a bare Connect-OER signs in
    interactively -- to connect the module again, which takes the session back,
    or use a new PowerShell process.

    If the session is closed with Disconnect-MgGraph instead of Disconnect-OER,
    the next OER cmdlet signs in again by itself, except on an app-only session
    (client secret or certificate), which reports
    AppOnlySessionCredentialUnavailable until Connect-OER is run with the secret
    or certificate.

    Runspaces in one process (ForEach-Object -Parallel, Start-ThreadJob) share
    one Graph SDK session, so a parallel fan-out across tenants in one process
    gets GraphSessionChanged; run each tenant in its own process instead, with
    Start-Job or a separate PowerShell process.

    A sign-in that fails or is refused usually leaves the module's session as
    it was -- the previous tenant's, or none -- and a script carries on past
    the error. From then on the module sends nothing for a command that names
    no tenant: such a command (no -TenantId, or -TenantId organizations) is
    refused with SignInRefused before it requests a token, and so are its
    requests and the sign-ins of the cmdlets it calls. A command that names
    its tenant signs in as usual. A successful sign-in by a command that names
    its tenant, a successful Connect-OER with or without a tenant, or
    Disconnect-OER makes the module send again. Before this release, when a
    Connect-OER in this loop failed, the Invoke-OERStructure after it applied
    that alias's document, -Prune included, in the previous tenant; now it
    sends nothing:

        foreach ($Alias in $Aliases) {
            Connect-OER -TenantAlias $Alias -ClientId $AppId -CertificatePath $Pfx
            Invoke-OERStructure -Path "$Alias.json" -Prune
        }

    Invoke-OERStructure without -TenantId likewise refuses every document
    piped to it after one whose sign-in was refused. A Connect-OER whose
    parameters PowerShell cannot bind never runs, so it leaves nothing behind
    for the module to see. An empty -TenantAlias, typed or piped, is refused
    with InvalidTenantAlias, and an empty, whitespace or $null -TenantId with
    InvalidTenantId; both count as a refused sign-in. Every other cmdlet
    refuses an empty or $null -TenantId while PowerShell binds its
    parameters, so that command never runs and sends nothing: an empty cell
    in a loop over tenants no longer stands for the current session's
    tenant. New-OERConfiguration and Set-OERConfiguration store the tenant
    instead of signing in to it, so they also refuse a -TenantId of white
    space only while PowerShell binds their parameters: a blank tenant can no
    longer be entered into a profile through them. A profile already on disk
    with one is still read as it is, and keeps it through an update that does
    not name -TenantId. On any other cmdlet, a -TenantId of spaces is looked
    up like any value that is not a tenant ID, and refused with
    TenantResolutionFailed when it names no tenant.

    One OER pipeline works in one tenant with one identity. If commands in the
    same pipeline sign in to different tenants or identities, a command whose
    sign-in another one replaced sends nothing more: every request made while
    it runs is refused with SignInSuperseded, including the requests of a
    command that handles its output, a ForEach-Object script block among
    them. Most OER cmdlets sign in before any command in the pipeline
    processes input, so that is usually the first command. A cmdlet that
    reports a failed lookup under an error of its own carries the refusal's
    message in that error instead -- New-OERGroup's GroupResolveFailed, for
    one. A tenant counts by the tenant its token was issued for, so naming one
    tenant by its GUID on one command and by its domain on another, or not
    naming it at all, is not refused for that alone. But a command that names
    the tenant differently from the session inherits nothing from it and
    signs in again -- interactively, when it names no credential, which on an
    app-only session means a browser prompt and another identity, and the
    pipeline is then refused as above. So name it the same way on every
    command, as "Name the tenant explicitly and consistently" under SOVEREIGN
    CLOUDS below says. Invoke-OERStructure signs in when it processes its
    document, not before, but without -TenantId it acts only under the
    session it began with: when another command in the same pipeline has
    signed in to a different tenant or identity by then -- any sign-in counts
    if the module held no session when it began -- it refuses that document
    with SignInSuperseded and sends nothing for it.
    New-OERAccessPackageApprovalStage, New-OERAccessPackageRequestorScope and
    New-OERAccessReviewStage do the same before they resolve the approvers,
    targets or reviewers they are given. Called inside a script block or a
    function in a pipeline, such as
    ForEach-Object { Invoke-OERStructure ... }, each of these commands
    begins only when that block runs, after every other command in the
    pipeline has begun and so after most of their sign-ins, and it takes the
    session they left for its own. A document exported by Get-OERInventory or
    Export-OERInventory names its tenant (tenantId, when it carries one), and
    Invoke-OERStructure refuses it in any other tenant with
    DocumentTenantMismatch, reading and writing nothing for it, whichever
    session it began with. A document without tenantId, and the three
    cmdlets above, still take that session for their own, so name -TenantId
    there. Run the commands as separate statements; to move objects between
    tenants, collect them in a variable first:

        # Read in one tenant, then write in another, as two statements
        $Filter = "startswith(displayName,'role_sec_')"
        $Groups = @(Get-OERGroup -TenantId $TenantA -Filter $Filter)
        foreach ($Group in $Groups) {
            New-OERGroup -TenantId $TenantB -DisplayName $Group.DisplayName
        }

        # Copy a structure the same way: read it, then apply it
        $Inventory = Get-OERInventory -TenantId $TenantA -Include Groups
        # The export names tenant A; apply it to B as a template
        $Inventory.PSObject.Properties.Remove('tenantId')
        Invoke-OERStructure -InputObject $Inventory -TenantId $TenantB -WhatIf

        # Not as one pipeline: New-OERGroup would run inside Get-OERGroup's
        # output and send nothing, and so would Invoke-OERStructure inside
        # Get-OERInventory's
        # Get-OERGroup -TenantId $TenantA ... |
        # ForEach-Object { New-OERGroup -TenantId $TenantB ... }
        # Get-OERInventory -TenantId $TenantA ... |
        # Invoke-OERStructure -TenantId $TenantB ...

    The second statement switches tenant in the same PowerShell process, so
    whether it reaches the tenant it names depends on the sign-in type, as
    SWITCHING TENANTS above describes.

    Invoke-OERStructure applies a document that names its tenant only in that
    tenant. Given a -TenantId that is another tenant ID, it refuses the
    document with DocumentTenantMismatch before it signs in, with no token
    request; otherwise, once it has signed in, it compares the document with
    the tenant the session's tokens were issued for. To apply an export in
    another tenant -- as a template, like the copy above -- change its
    tenantId to that tenant's ID or remove the key first.

SOVEREIGN CLOUDS

    Connect-OER -Environment selects the sovereign cloud a session signs in to and
    calls. Four values are supported:

        Global (default) Worldwide commercial cloud
                           graph.microsoft.com / management.azure.com
        USGov GCC High
                           graph.microsoft.us / management.usgovcloudapi.net
        USGovDoD DoD
                           dod-graph.microsoft.us / management.usgovcloudapi.net
        China 21Vianet
                           microsoftgraph.chinacloudapi.cn / management.chinacloudapi.cn

    Microsoft 365 GCC uses the commercial (Global) endpoints and needs no -Environment
    at all. Only GCC High, DoD and a 21Vianet tenant are separate cloud boundaries; GCC
    itself is not.

    The chosen cloud becomes part of the session: every later cmdlet calls the cloud the
    session was established in, and switching clouds re-authenticates instead of reusing
    a token minted at the previous cloud's authority.

        Connect-OER -TenantId 'contoso.onmicrosoft.us' -Environment USGov -IncludeARM

    A Tenant Profile's optional Environment key stores the cloud for that tenant, so
    Connect-OER -TenantAlias picks it up automatically without repeating -Environment on
    every call; an explicit -Environment on the command line still overrides it.

    Name the tenant explicitly and consistently. The session is keyed on the tenant
    exactly as you spell it, so a later call naming a tenant the session was not
    established for inherits nothing from it -- including the same tenant written as a
    GUID one time and as a domain the next, and a Connect-OER -Environment USGov with no
    -TenantId at all, which records the tenant as 'organizations' rather than as yours.
    The cloud then resets to Global and that call signs in at the commercial authority.
    Its token and its endpoints stay consistent with each other, so nothing crosses a
    cloud boundary, but the sign-in fails at the authority with an error that names the
    tenant and never the cloud, which makes it easy to misread. (A tenant named by
    domain is looked up at that same authority first, and may be refused there instead,
    with TenantResolutionFailed, whose message does mention -Environment.) Pass the same
    -TenantId spelling on every call, or use a Tenant Profile whose Environment key
    carries the cloud for you.

    Known limitation: PIM-for-Groups is pinned to the Microsoft Graph beta endpoint (see
    PERMISSIONS above), and beta endpoint availability in US Government and China clouds
    is not established. A sovereign tenant that needs PIM-for-Groups may find that pinned
    beta path behaves differently, or not at all, from the commercial cloud this module is
    built and tested against.

AZURE RESOURCE TARGETING

    Azure RBAC and PIM assignment cmdlets (New/Get-OERRoleAssignment,
    New/Get/Remove-OEREligibleRoleAssignment, New/Get/Remove-OERActiveRoleAssignment,
    Enable/Disable-OEREligibleRoleAssignment) accept a resource scope in three forms:

    - Raw scope id: -Scope '<full ARM resource id>'
    - Friendly decomposition: -Subscription <s> -ResourceGroup <rg>
      -ResourceType <type> -ResourceName <name>
    - Management group: -ManagementGroup <name-or-id>

    Get-OERRoleManagementPolicy and Set-OERRoleManagementPolicy accept the same scope
    forms minus -ResourceType and -ResourceName, because a role management policy is
    assigned at a management group, subscription or resource group scope. By contrast
    Set-OERRoleAssignment and Remove-OERRoleAssignment take -Id only: they act on one
    existing assignment, which already carries its own scope.

    Get-OERResource lists Azure resources within a subscription or resource group
    (client-side -Name/-ResourceType filters) and, with -IncludeRoleAssignments
    (optionally -ResolveNames), reports the role-assignment delegations on each resource.
    Output is tagged Omnicit.EntraRBAC.Resource and binds directly into the assignment
    cmdlets by property name -- for example:

        Get-OERResource -Subscription Prod -ResourceGroup rg-app -Name stgfoo |
            New-OERRoleAssignment -Role Reader -Group role_sec_readers

    Get-OERManagementGroup lists the management groups the caller can read, each with its parent
    (ParentId, ParentName, ParentDisplayName), read with at most one further call per list. Only
    the tenant root group has none. A parent that cannot be read is reported, after the groups,
    as one non-terminating ManagementGroupParentReadFailed error naming those groups, rather than
    left silently empty. A caller that can read no management group is refused by the list
    (AuthorizationFailed), not given an empty list. -Expand (the direct children) and -Recurse
    (the whole hierarchy) read below ONE group, so they need -Name: without it the call fails
    at parameter binding, or PowerShell asks for -Name where the host can prompt.

ARM OBJECT IDS

    Get-OERResource, Get-OERResourceGroup, Get-OERSubscription and Get-OERManagementGroup expose
    the full ARM resource path as ResourceId, never as a bare Id. This is deliberate: -PolicyId on
    Get-OERRoleManagementPolicy/Set-OERRoleManagementPolicy and -RoleEligibilityScheduleId on
    Enable-OEREligibleRoleAssignment both carry an Id alias and bind from the pipeline by property
    name, so a bare Id on an ARM-resource object would silently mis-route as a policy id or an
    eligibility schedule id instead of the resource path it actually is.

    Get-OERRoleDefinition follows the same no-bare-Id rule but stores the ARM role definition path
    the other way round: RoleDefinitionId is the one STORED value (so a piped role definition still
    binds -Role on New-OERRoleAssignment, and its PIM siblings, through that alias), and ResourceId
    is exposed as an AliasProperty of it rather than a second stored copy. Either property reads
    the same value; there is deliberately no bare Id that could mis-bind downstream.

    Role-ASSIGNMENT-shaped objects (RoleAssignment, EligibleRoleAssignment, ActiveRoleAssignment)
    are a different case: they identify an assignment INSTANCE, not an ARM resource, so their own
    domain-specific id (RoleAssignmentId, RoleEligibilityScheduleId, RoleAssignmentScheduleId) is
    the right property to carry, not ResourceId. Set-OERRoleAssignment and Remove-OERRoleAssignment
    act on -Id alone (the RoleAssignmentId, which already embeds the scope) rather than on the
    friendly -Subscription/-ResourceGroup/-ManagementGroup decomposition above.

ACCESS PACKAGE ASSIGNMENT POLICY -- GRANULAR SETTINGS

    New-OERAccessPackageRequestorSettings builds a requestor-settings object that
    controls how users and managers may submit requests:

        $Rs = New-OERAccessPackageRequestorSettings -AllowSelfRequest `
                  -AllowManagerRequest -ManagerLevel 1 -AllowSelfExtend

    All -Allow* parameters are switches. Pass the result to
    New-OERAccessPackageAssignmentPolicy or Set-OERAccessPackageAssignmentPolicy
    via -RequestorSettings.

    Both policy cmdlets also accept (the toggles are switches):

    -RequireApproval -- enable the approval workflow
    -RequireRequestorJustification -- require a justification from the requestor
    -RequireApprovalForUpdate -- require approval for extension requests
    -DurationInHours <int> -- assignment lifetime in hours (PT{n}H)
    -DisableAssignmentNotifications -- suppress built-in assignment emails

    New-OERAccessPackageApprovalStage accepts -ApproverInfoVisibility
    (Default / Visible / NotVisible) to control whether approver identity is
    shown to the requestor.

    Set-OERAccessPackageAssignmentPolicy is a read-modify-write (GET then PUT):
    fields you do not supply are preserved from the live policy (including the
    out-of-scope reviewSettings and custom questions). On both
    New- and Set-OERAccessPackageAssignmentPolicy the Description parameter
    defaults to the policy display name when omitted.

    Limitations: policy-embedded access reviews (reviewSettings) and custom
    requestor questions cannot be expressed in an apply document -- they are
    preserved by the read-modify-write. Fallback approvers are preserved when
    their stage is unchanged but are dropped when the stage is rebuilt.

DURATION VOCABULARY

    Most cmdlets that accept a lifetime or expiration share the same duration
    vocabulary. (A few narrowly-scoped parameters, such as
    Set-OERRoleManagementPolicy -ActivationMaxHours and
    New-OERAccessPackageApprovalStage -EscalationDays, keep their own
    already-unit-specific name instead.)

    -Duration <string> -- raw ISO 8601 duration (P365D, PT8H)
    -DurationDays <int> -- whole days, converted to P{n}D
    -DurationHours <int> -- whole hours, converted to PT{n}H

    -DurationInDays / -DurationInHours (access packages, access reviews) and
    -EligibleDuration / -ActiveDuration (PIM policies) are the canonical
    parameter names; -DurationDays / -DurationHours and
    -EligibleDurationDays / -ActiveDurationDays are the module-standard
    aliases this unification added.

    Exception: Add-OERGroupEligibility -Duration also accepts a bare whole
    day count (-Duration 365 means 365 days) for back-compat with its
    original shipped signature. This is deliberate and scoped to that one
    cmdlet -- New-OEREligibleRoleAssignment -Duration, by contrast, validates
    strictly against the ISO 8601 pattern and rejects a bare number such as
    365 outright.

SEE ALSO
    Connect-OER
    Get-OERConfiguration
    Get-OERGroup
    Get-OERAdministrativeUnit
    Get-OERAccessPackage
    Get-OERAccessReviewDefinition
    Get-OERRoleAssignment
    Get-OEREligibleRoleAssignment
    Get-OERRoleManagementPolicy
    Get-OERInventory
    Invoke-OERStructure