365TUNE.psd1

@{
    ModuleVersion     = "2.3.16"
    GUID              = "7c9c4650-e980-44ca-8092-50e0ae0ec9bc"
    Author            = "Metawise Consulting LLC"
    CompanyName       = "Metawise Consulting LLC"
    Copyright         = "(c) 2025 Metawise Consulting LLC. All rights reserved."
    Description       = "365TUNE PowerShell toolkit for Microsoft 365 license optimization, security compliance, cost reporting, and Azure permissions management."
    RootModule        = "365TUNE.psm1"
    FunctionsToExport = @(
        "Invoke-365TUNEConnectAll",
        "Invoke-365TUNERevokeAll",
        "Invoke-365TuneConnectAzure",
        "Invoke-365TuneRevokeAzure",
        "Invoke-365TuneConnectExchange",
        "Invoke-365TuneRevokeExchange",
        "Invoke-365TuneConnectTeams",
        "Invoke-365TuneRevokeTeams",
        "Invoke-365TuneTestAzure",
        "Invoke-365TuneTestExchange",
        "Invoke-365TuneTestTeams",
        "Invoke-365TUNETestAll"
    )
    CmdletsToExport   = @()
    AliasesToExport   = @()
    VariablesToExport = @()
    PrivateData       = @{
        PSData = @{
            Tags         = @("Azure","Microsoft365","M365","MSP","Licensing","Security","Compliance","CIS","Entra","GDAP","ExchangeOnline","365TUNE")
            ProjectUri   = "https://365tune.com"
            LicenseUri   = "https://365tune.com/Gallerylicense"
            ReleaseNotes = @"
v2.3.16 (bug fix release)
- Fixed Remove-365TuneElevation for Cloud Shell CLI bridge: the CLI ARM token works for GET and
  POST (elevateAccess) but not DELETE at root scope. Replaced the retry-delay approach with
  three escalating methods: (1) Invoke-WebRequest with CLI ARM token using api-version=2022-04-01,
  (2) az rest --method DELETE (CLI native HTTP client, handles auth differently), and
  (3) Remove-AzRoleAssignment Az cmdlet. Returns on first success.

v2.3.15 (bug fix release)
- Fixed Remove-365TuneElevation: UAA DELETE at root scope gets 403 when RBAC propagation is
  incomplete. Root cause: the GET to find the assignment works immediately (GA can always list
  root-scope assignments), but the authorization service may take 30-120 seconds to reflect
  UAA for write/delete operations. Replaced the single 20+20s retry with a retry loop:
  30s initial wait then up to 3 attempts, each 30s apart (90s max), with status feedback.

v2.3.14 (bug fix release)
- Fixed Invoke-365TuneElevation and Remove-365TuneElevation: replaced Invoke-AzRestMethod with
  direct Invoke-WebRequest calls using ARM tokens fetched via Get-AzAccessToken with az CLI
  fallback. Invoke-AzRestMethod has the same static-token limitation as Get-AzAccessToken when
  the context is established via Connect-AzAccount -AccessToken (CLI bridge) -- it sends no
  valid auth header, resulting in a 401 from the elevateAccess endpoint.

v2.3.13 (bug fix release)
- Fixed Cloud Shell CLI bridge token propagation (all 7 affected functions): when authentication
  uses the Azure CLI bridge (az account get-access-token + Connect-AzAccount -AccessToken), the
  resulting static-token context cannot issue tokens for other resources via Get-AzAccessToken
  (no refresh token). Every Get-AzAccessToken call is now wrapped in try/catch; if it throws,
  the catch re-fetches the token directly from az account get-access-token. This covers both
  standalone function calls and calls from orchestrators via -SkipAuth.

v2.3.12 (bug fix release)
- Fixed Cloud Shell MSI handling (all 12 functions): when Get-AzContext -ListAvailable
  returns no user context, the module now bridges from Azure CLI which is always
  authenticated as the portal user in Cloud Shell. Uses az account get-access-token to
  obtain ARM and Graph tokens, then calls Connect-AzAccount -AccessToken to establish the
  user context in Az PowerShell without any interactive prompt or device code.

v2.3.11 (bug fix release)
- Fixed Cloud Shell MSI handling (all 12 functions): replaced Connect-AzAccount with
  Get-AzContext -ListAvailable + Set-AzContext. When Cloud Shell is opened from the Azure
  Portal, it injects the user's context alongside MSI but MSI may be set as active. The
  fix finds the first non-MSI context in the list and switches to it without any login
  prompt. Falls back to Connect-AzAccount only if no user context is present at all.

v2.3.10 (bug fix release)
- Fixed Cloud Shell MSI handling (all 12 functions): when Cloud Shell starts with MSI
  context, functions now call Connect-AzAccount WITHOUT calling Disconnect-AzAccount first.
  This keeps MSI as a fallback while attempting browser-based (portal session) login.
  The v2.3.8 approach called Disconnect first, which stripped the MSI fallback and caused
  Connect-AzAccount to fail silently, leaving no context at all.
- Fixed Remove-365TuneElevation MSI account quirk: elevation cleanup used SignInName to
  find the User Access Administrator assignment, but MSI/SP accounts have no SignInName so
  the assignment was never found and left in place. Lookup now uses a REST API call filtered
  by principalId (OID from the JWT), which works for both user and MSI/SP accounts. The
  Az cmdlet is retained as a fallback.

v2.3.8 (bug fix release)
- Fixed Cloud Shell MSI fallback (all 12 functions): when Cloud Shell is opened outside
  the Azure Portal the pre-loaded context is MSI rather than a user account. All functions
  now detect this and call Connect-AzAccount (browser-based, no device code) to obtain a
  real user context before proceeding instead of throwing immediately.

v2.3.7 (bug fix release)
- Fixed Cloud Shell MSI authentication (all 12 functions): Connect-AzAccount in Cloud Shell
  was replacing the pre-loaded user context with the Cloud Shell Managed Service Identity.
  All functions now skip Connect-AzAccount in Cloud Shell and use the existing user context.
  Added MSI guard: throws a clear error if Cloud Shell context is MSI instead of a user.

v2.3.6 (bug fix release)
- Fixed UnsupportedFilter error in TestAzure, ConnectAzure, and RevokeAzure: root-scope
  role assignment queries now include principalId filter required by the Azure ARM API.
  Unfiltered queries at '/' scope are rejected by the API with UnsupportedFilter.

v2.3.5 (bug fix release)
- Fixed Cloud Shell authentication: all functions now authenticate as the signed-in user
  instead of the VM Managed Identity (MSI). MSI has no Graph API or Exchange Admin
  permissions, causing all Cloud Shell runs to fail silently or throw.
- Fixed 5 misplaced backtick line-continuations in ConnectExchange, ConnectTeams,
  RevokeTeams that broke command parsing and caused ParameterBindingExceptions.
- Fixed -TimeoutSec and -ErrorAction Stop being bound to Where-Object instead of
  Invoke-RestMethod in ConnectAzure and RevokeAzure (6 occurrences).
- Fixed -ErrorAction Stop applied to Out-Null instead of Invoke-RestMethod in
  ConnectExchange and ConnectTeams, causing silent swallowing of HTTP errors.
- Fixed RevokeAzure: REST queries between elevation and try/finally block were
  unprotected - if they threw, User Access Administrator elevation was never removed.
  All post-elevation code is now inside a single try/finally block.
- Updated ConnectExchange and RevokeExchange .DESCRIPTION: now supported in Cloud Shell.
"@

        }
    }
}