lib/Classes/Public/TMSession.ps1
class TMSession { #region Non-Static Properties # A Name parameter to identify the session in other functions [String]$Name = 'Default' # TM Server hostname [String]$TMServer # TMVersion drives the selection of compatible APIs to use [Version]$TMVersion = [Version]::new() # Logged in TM User's Context [TMSessionUserContext]$UserContext = [TMSessionUserContext]::new() # TMWebSession Variable. Maintained by the Invoke-WebRequest function's capability [Microsoft.PowerShell.Commands.WebRequestSession]$TMWebSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new() # TMRestSession Variable. Maintained by the Invoke-RestMethod function's capability [Microsoft.PowerShell.Commands.WebRequestSession]$TMRestSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new() # Tracks non-changing items to reduce HTTP lookups, and increase speed of scripts. # DataCache is expected to be a k/v pair, where the V could be another k/v pair, # However, it's implementation will be more of the nature to hold the list of object calls from the API # like 'credentials' = @(@{...},@{...}); 'actions' = @(@{...},@{...}) # Get-TM* functions will cache unless a -NoCache switch is provided [Hashtable]$DataCache = @{} # Holds the various tokens used to authenticate with TM hidden [TMSessionAuthToken]$AuthTokens = [TMSessionAuthToken]::new() # A timer which will fire and event every 10 minutes to check if the bearer token needs to be refreshed hidden [Timers.Timer]$TokenRefreshTimer = [Timers.Timer]::new() # The number of seconds before the bearer token expires when the refresh should occur (Default: 1 hour) hidden [Int32]$TokenRefreshLeadTime = 3600 # Should PowerShell ignore the SSL Cert on the TM Server? [Boolean]$AllowInsecureSSL = $false #endregion Non-Static Properties #region Constructors TMSession() { $this.addPublicMembers() $this.tokenRefreshEventSetup() } TMSession([String]$name) { $this.Name = $name $this.addPublicMembers() $this.tokenRefreshEventSetup() } TMSession([String]$name, [String]$server, [Boolean]$allowInsecureSSL) { $this.Name = $name $this.TMServer = [TMSession]::FormatServerUrl($server) $this.AllowInsecureSSL = $allowInsecureSSL $this.addPublicMembers() $this.tokenRefreshEventSetup() } # Constructor used by Import-TMCActionRequest TMSession([PSObject]$actionrequest) { $this.addPublicMembers() $this.tokenRefreshEventSetup() # Assign Values from the passed ActionRequest $this.TMServer = [TMSession]::FormatServerUrl($actionRequest.options.callback.siteUrl) $Version = [Version]::new() if ([Version]::TryParse(($actionrequest.tmUserSession.tmVersion -split '-')[0], [ref]$Version)) { $this.TMVersion = $Version } $this.UserContext = [TMSessionUserContext]::new($actionrequest.tmUserSession.userContext) # Extract all of the Auth tokens and apply them $this.AuthTokens.JSessionId = $actionrequest.tmUserSession.jsessionid $this.AuthTokens.BearerToken = $actionrequest.options.callback.token $this.AuthTokens.RefreshToken = $actionrequest.options.callback.refreshToken $this.AuthTokens.ExpirationSeconds = $actionrequest.options.callback.expirationSeconds $this.AuthTokens.CsrfHeaderName = $actionrequest.tmUserSession.csrf.tokenHeaderName $this.AuthTokens.CsrfToken = $actionrequest.tmUserSession.csrf.token $this.AuthTokens.PublicKey = $actionrequest.publicRSAKey $this.ApplyHeaderTokens() } #endregion Constructors #region Non-Static Methods <# Summary: Takes the items from the AuthTokens object and applies them to the appropriate headers/cookies in the web sessions Params: None Outputs: None #> [void]ApplyHeaderTokens() { # Add the JSESSIONID Cookie if (-not [String]::IsNullOrWhiteSpace($this.AuthTokens.JSessionId)) { $JSessionCookie = [Net.Cookie]::new() $JSessionCookie.Name = "JSESSIONID" $JSessionCookie.Value = $this.AuthTokens.JSessionId $JSessionCookie.Domain = $this.TMServer if (-not ($this.TMWebSession.Cookies.GetAllCookies() | Where-Object Name -eq 'JSESSIONID')) { $this.TMWebSession.Cookies.Add($JSessionCookie) } if (-not ($this.TMRestSession.Cookies.GetAllCookies() | Where-Object Name -eq 'JSESSIONID')) { $this.TMRestSession.Cookies.Add($JSessionCookie) } } # Add the Bearer Token header if (-not [String]::IsNullOrWhiteSpace($this.AuthTokens.BearerToken)) { $this.TMRestSession.Headers.Authorization = "Bearer $($this.AuthTokens.BearerToken)" } # Add the CSRF header if (-not [String]::IsNullOrWhiteSpace($this.AuthTokens.CsrfToken) -and -not [String]::IsNullOrWhiteSpace($this.AuthTokens.CsrfHeaderName)) { $this.TMWebSession.Headers."$($this.AuthTokens.CsrfHeaderName)" = $this.AuthTokens.CsrfToken $this.TMRestSession.Headers."$($this.AuthTokens.CsrfHeaderName)" = $this.AuthTokens.CsrfToken } } <# Summary: Parses the response from the login endpoints to collect and format its data into the class instance Params: Response - The response object returned from the login endpoints Endpoint - The login endpoint that the request was sent to (WS or REST) Outputs: None #> [void]ParseLoginResponse([Object]$Response, [String]$Endpoint) { # Convert the response content if necessary if ($Response -is [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]) { $Response = $Response.Content | ConvertFrom-Json -Depth 10 } switch ($Endpoint) { 'WS' { # Add the necessary properties from the web service login response to the class instance $this.UserContext = [TMSessionUserContext]::new($Response.userContext) $this.AuthTokens.CsrfHeaderName = $Response.csrf.tokenHeaderName ?? 'X-CSRF-TOKEN' $this.AuthTokens.CsrfToken = $Response.csrf.token $this.AuthTokens.SsoToken = $Response.reporting.ssoToken $this.AuthTokens.JSessionId = ($this.TMWebSession.Cookies.GetAllCookies() | Where-Object Name -eq JSESSIONID | Select-Object -First 1).Value } 'REST' { # Add the necessary properties from the REST API login response to the class instance $this.AuthTokens.BearerToken = $Response.access_token $this.AuthTokens.RefreshToken = $Response.refresh_token $this.AuthTokens.ExpirationSeconds = $Response.expires_in $this.AuthTokens.GrantedDate = Get-Date } } $this.ApplyHeaderTokens() } <# Summary: Requests a new bearer token from TM Params: None Outputs: None #> [void]RefreshBearerToken() { $this | Update-TMSessionToken } #endregion Non-Static Methods #region Private Methods <# Summary: Creates an event handler for the token refresh timer Params: None Outputs: None #> hidden [void]tokenRefreshEventSetup() { $EventId = $this.Name + "_TMSession.TokenRefreshTimer.Elapsed" # Remove any old event subscriptions try { Unregister-Event -SourceIdentifier $EventId -Force -ErrorAction Ignore } catch {} # Create a new event handler for the timer 'Elapsed' event $EventSplat = @{ InputObject = $this.TokenRefreshTimer EventName = 'Elapsed' SourceIdentifier = $EventId MessageData = @{ this = $this } Action = { if ($Event.MessageData.this.ShouldRefreshBearerToken) { $Event.MessageData.this.RefreshBearerToken() } } } Register-ObjectEvent @EventSplat # Set the timer to tick every 10 minutes $this.TokenRefreshTimer.Interval = 600000 $this.TokenRefreshTimer.Start() } <# Summary: Adds members with calculated get and/or set methods Params: None Outputs: None #> hidden [void]addPublicMembers() { # Add a read-only calculated ShouldRefreshBearerToken property $this.PSObject.Properties.Add( [PSScriptProperty]::new( 'ShouldRefreshBearerToken', { # get return (((Get-Date) - $this.AuthTokens.GrantedDate).TotalSeconds -ge ($this.AuthTokens.ExpirationSeconds - $TokenRefreshLeadTime)) } ) ) } #endregion Private Methods #region Static Methods <# Summary: Formats the passed URL to be compatible with TM module functions Params: Url - The URL to be formatted Outputs: The URL stripped down to just the host name #> static [String]FormatServerUrl([String]$Url) { return (([URI]$Url).Host ?? $Url -replace '/tdstm.*|https?://', '') } #endregion Static Methods } class TMSessionAuthToken { #region Non-Static Properties # The JSessionID cookie provided by the web service login response [String]$JSessionId # The name of the CSRF header [String]$CsrfHeaderName = 'X-CSRF-TOKEN' # The CSRF token provided by the web service login response [String]$CsrfToken # The access token provided by the REST API login response [String]$BearerToken # The refresh token provided by the REST API login response. Will be used to get a new access token before expiration [String]$RefreshToken # The SSO token provided by the web service login response [String]$SsoToken # TM's public key [String]$PublicKey # The number of seconds that the REST access token is valid for [Int32]$ExpirationSeconds = [Int32]::MaxValue # The date/time the REST access token was granted [Nullable[DateTime]]$GrantedDate = (Get-Date) #endregion Non-Static Properties #region Constructors TMSessionAuthToken() { $this.addPublicMembers() } #endregion Constructors #region Private Methods <# Summary: Adds members with calculated get and/or set methods Params: None Outputs: None #> hidden [void]addPublicMembers() { # Add a read-only calculated ExpirationDate property $this.PSObject.Properties.Add( [PSScriptProperty]::new( 'ExpirationDate', { # get $this.GrantedDate.AddSeconds($this.ExpirationSeconds) } ) ) } #endregion Private Methods } class TMSessionUserContext { #region Non-Static Properties [TMReference]$User [TMSessionUserContextPerson]$Person [TMSessionUserContextProject]$Project [TMReference]$Event [Object]$Bundle [String]$Timezone [String]$DateFormat [Boolean]$GridFilterVisible [String[]]$AlternativeProjects [String]$DefaultLandingPage [String]$LandingPage #endregion Non-Static Properties #region Constructors TMSessionUserContext() {} TMSessionUserContext([Object]$object) { $this.User = [TMReference]::new($object.user) $this.Person = [TMSessionUserContextPerson]::new($object.person) $this.Project = [TMSessionUserContextProject]::new($object.project) $this.Event = [TMReference]::new($object.event) $this.Bundle = $object.bundle $this.Timezone = $object.timezone $this.DateFormat = $object.dateFormat $this.GridFilterVisible = $object.gridFilterVisible $this.AlternativeProjects = $object.alternativeProjects $this.DefaultLandingPage = $object.defaultLandingPage $this.LandingPage = $object.landingPage } #endregion Constructors } class TMSessionUserContextPerson { #region Non-Static Properties [Int64]$Id [String]$FirstName [String]$FullName #endregion Non-Static Properties #region Constructors TMSessionUserContextPerson() {} TMSessionUserContextPerson([Int64]$id, [String]$firstName, [String]$fullName) { $this.Id = $id $this.FirstName = $firstName $this.FullName = $fullName } TMSessionUserContextPerson([Object]$object) { $this.Id = $object.id $this.FirstName = $object.firstName $this.FullName = $object.fullName } #endregion Constructors } class TMSessionUserContextProject { #region Non-Static Properties [Int64]$Id [String]$Name [String]$Status [String]$LogoUrl [String]$Code #endregion Non-Static Properties #region Constructors TMSessionUserContextProject() {} TMSessionUserContextProject([Int64]$id, [String]$name, [String]$status, [String]$logoUrl, [String]$code) { $this.Id = $id $this.Name = $name $this.Status = $status $this.LogoUrl = $logoUrl $this.Code = $code } TMSessionUserContextProject([Object]$object) { $this.Id = $object.id $this.Name = $object.name $this.Status = $object.status $this.LogoUrl = $object.logoUrl $this.Code = $object.code } #endregion Constructors } |