Modules/businessdev.ALbuild.Core/Public/Test-ALbuildLicense.ps1
|
function Test-ALbuildLicense { <# .SYNOPSIS Verifies an ALbuild commercial license against the licensing service. .DESCRIPTION Free-tier functionality performs no license check. Licensed features call this to verify a tenant's license via the 365 business development licensing service. The result is cached per tenant for the session. Network/service failures do NOT throw - they return an object with IsValid = $false and a Reason - so that license enforcement is an explicit decision of the caller (see Assert-ALbuildLicensed), never an accidental side effect. Two offline accommodations exist for trusted agents that cannot always reach the service: * ALBUILD_LICENSE_KEY - when this environment variable is set, the tenant is authorized without contacting the service (set it as a secret on agents in locked-down networks). * Grace window - a successful verification is cached to disk; if the service is later unreachable, that cached result keeps the feature working for LicenseGraceDays days (config, default 14). An explicit invalid/expired response is never graced (the cache is cleared), so a revoked license cannot keep working offline. .PARAMETER TenantId The Azure DevOps organization / collection id. Defaults to $env:System_CollectionId. .PARAMETER TenantName The Azure DevOps organization / collection URI. Defaults to $env:System_CollectionUri. .PARAMETER Refresh Bypass the per-tenant session cache and re-query the service. .EXAMPLE Test-ALbuildLicense -TenantId $env:System_CollectionId .OUTPUTS PSCustomObject with IsValid, Status, IsTrial, ExpiresOn, TenantId, TenantName, Reason. #> [CmdletBinding()] param( [string] $TenantId = $env:System_CollectionId, [string] $TenantName = $env:System_CollectionUri, [switch] $Refresh ) $normalizedName = if ($TenantName) { $TenantName -replace '^https?://', '' -replace '/+$', '' } else { '' } # Offline override for trusted agents that cannot reach the licensing service (e.g. a self-hosted # agent in a locked-down customer network). The organization sets ALBUILD_LICENSE_KEY as a secret # on those agents; its presence authorizes the licensed features without contacting the service. if (-not [string]::IsNullOrWhiteSpace($env:ALBUILD_LICENSE_KEY)) { return [PSCustomObject]@{ IsValid = $true; Status = 'OfflineKey'; IsTrial = $false; ExpiresOn = $null TenantId = $TenantId; TenantName = $normalizedName Reason = 'Authorized by the offline license key (ALBUILD_LICENSE_KEY).' } } if ([string]::IsNullOrWhiteSpace($TenantId)) { return [PSCustomObject]@{ IsValid = $false; Status = 'NoTenant'; IsTrial = $false; ExpiresOn = $null TenantId = $TenantId; TenantName = $TenantName Reason = 'No tenant id available (set -TenantId, ALBUILD_LICENSE_KEY, or run inside Azure DevOps).' } } if (-not $script:ALbuildLicenseCache) { $script:ALbuildLicenseCache = @{} } if (-not $Refresh -and $script:ALbuildLicenseCache.ContainsKey($TenantId)) { return $script:ALbuildLicenseCache[$TenantId] } $baseUrl = (Get-ALbuildConfig -Name 'LicensingBaseUrl').TrimEnd('/') $appId = Get-ALbuildConfig -Name 'LicenseAppId' $url = "$baseUrl/v1/apps/$appId/features/$appId/tenant/$TenantId/verifyLicense" # Read a property defensively so a partial response (e.g. an explicit denial with no 'license' # node) yields a clean result under Set-StrictMode instead of throwing into the unreachable path. function Get-Field([object] $Object, [string] $Name) { if ($null -eq $Object) { return $null } $prop = $Object.PSObject.Properties[$Name] if ($prop) { $prop.Value } else { $null } } try { $response = Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 30 -ErrorAction Stop $license = Get-Field $response 'license' $status = [string](Get-Field $response 'status') $isOk = $status -and ($status.ToLowerInvariant() -eq 'ok') $licenseKey = [string](Get-Field $license 'licenseKey') $isTrial = $isOk -and ($null -ne $license) -and [string]::IsNullOrEmpty($licenseKey) $expires = $null $trialEnd = Get-Field $license 'trialPeriodEndingDate' if ($isOk -and $trialEnd) { [datetime] $parsed = [datetime]::MinValue if ([datetime]::TryParse([string]$trialEnd, [ref] $parsed)) { $expires = $parsed } } $result = [PSCustomObject]@{ IsValid = [bool]$isOk Status = if ($status) { $status } else { 'Unknown' } IsTrial = [bool]$isTrial ExpiresOn = $expires TenantId = $TenantId TenantName = $normalizedName Reason = if ($isOk) { '' } else { "Licensing service returned status '$status'." } } # Persist a valid verification for the offline grace window; drop the cache on an explicit # denial so a revoked license cannot keep working offline. if ($isOk) { Save-ALbuildLicenseCacheEntry -TenantId $TenantId -Status $status -IsTrial ([bool]$isTrial) -ExpiresOn $expires } else { Save-ALbuildLicenseCacheEntry -TenantId $TenantId -Clear } } catch { # Service unreachable (network/timeout). Fall back to a previously-verified license within the # grace window so a trusted agent that has verified before keeps working through a transient # outage. (An agent that can never reach the service should use ALBUILD_LICENSE_KEY.) $graceDays = [int](Get-ALbuildConfig -Name 'LicenseGraceDays') $cached = if ($graceDays -gt 0) { Get-ALbuildLicenseCacheEntry -TenantId $TenantId } else { $null } if ($cached -and ((((Get-Date) - $cached.VerifiedAt).TotalDays) -le $graceDays)) { $ageDays = [int]((Get-Date) - $cached.VerifiedAt).TotalDays Write-ALbuildLog -Level Warning "Licensing service unreachable ($($_.Exception.Message)); using the cached license verified $ageDays day(s) ago (grace window $graceDays days)." $result = [PSCustomObject]@{ IsValid = $true; Status = 'Cached'; IsTrial = $cached.IsTrial; ExpiresOn = $cached.ExpiresOn TenantId = $TenantId; TenantName = $normalizedName Reason = 'Using the cached license (licensing service unreachable).' } } else { $result = [PSCustomObject]@{ IsValid = $false; Status = 'Error'; IsTrial = $false; ExpiresOn = $null TenantId = $TenantId; TenantName = $normalizedName Reason = "Could not reach the licensing service: $($_.Exception.Message). Set ALBUILD_LICENSE_KEY on a trusted agent that cannot reach the service." } } } $script:ALbuildLicenseCache[$TenantId] = $result return $result } |