## Crowd-ApplicationAuthenticate ## Aaron Bockelie ## aaron.bockelie@wbgames.com <# .SYNOPSIS http://docs.atlassian.com/crowd/current/com/atlassian/crowd/service/soap/server/SecurityServer.html #> function Crowd-ApplicationAuthenticate {param([Parameter(Mandatory=$true)][string]$crowdapp,[Parameter(Mandatory=$true)][string]$crowdpass) $erroractionpreference = 'continue' $warningpreference = 'continue' $eventLogSource = "Perforce Authentication for Crowd" ## first section ## we're going to attempt to acquire required resources to make this work. This is the crowd WSDL descriptor file. ## we try to cache it so we don't have to make as many calls. Optimizing this process is important, so we can speed up authentication. $wsdlpath = gl | %{$_.path} #take current operating path $wsdlfilenamepath = $null #null out this so we can test without false positives if (!$env:crowdWSDLurl) #if the system environment variable crowdwsdlurl doesn't exist, we need to define it. { Get-CrowdWSDLURL #go define it. write-host "If the Crowd WSDL url appeared to be configured correctly, please try authenticating again." break } else { $wsdlurl = $env:crowdWSDLurl #if we do have that environment variable, let's copy it into wsdlurl. } try { $wsdlfile = $wsdlurl.split("/")[2]+".wsdl" #invent a wsdl cache file name from the url given. } catch { $wsdlfile = $wsdlurl.split(".")[0]+".wsdl" #invent a wsdl cache file name from a file name given. } $wsdlfilenamepath = $wsdlpath + "\" + $wsdlfile #create an object that has the full path and name of the file. if ((test-path $wsdlfilenamepath) -eq $false) #test to see if a cached object does not exist. We will make one if it is not found. { $client = new-object System.Net.WebClient #new internet object try { $message = "No WSDL file found for given SOAP service. Attempting to cache wsdl file." #set up the event log message Send-EventMessage -eventMessage $message -eventType "Warning" -eventid 38010 -eventlogSource $eventLogSource #send our message write-verbose $message $client.DownloadFile($wsdlurl,$wsdlfilenamepath)#attempt to store file in directory function ran from. if ((test-path $wsdlfilenamepath) -eq $true) #if the file was created { $message = "WSDL file cached to " + $wsdlfilenamepath #send a message to event viewer stating cache file was created. Send-EventMessage -eventMessage $message -eventType "Warning" -eventid 38010 -eventlogSource $eventLogSource write-warning $message } else #otherwise { $message = "WSDL File was not written to the file system. The path was " + $wsdlfilenamepath + ".`r`n`r`n" + + $error[0] #we couldn't write the cache file to the current directory. Send-EventMessage -eventMessage $message -eventType "Error" -eventid 38020 -eventlogSource $eventLogSource write-error $message -category "WriteError" } } catch { $message = "FATAL: Couldn't cache WSDL definition file to current operating directory. Cached copy is highly recommended.`r`n`r`n" + + $error[0] #we couldn't cache the file for some other reason. Send-EventMessage -eventMessage $message -eventType "Error" -eventid 38020 -eventlogSource $eventLogSource write-error $message #failing to cache some sort of content from the server at this point will stop the function, because we really want to be able to use a cached copy. } } ## second section ## Now, we will attempt to build our wsdl service object with the cached uri. ## After that, we will attempt to authenticate our "application" with the given credentials, and cache the credential file as a powershell ## xml object. The idea is that when run later we will test and re-use the credential. If the credential doesn't exist, or is invalid, ## we'll have to dial out to the soap object and retrieve then cache a current credential object. if (!$global:crowdService.url)#if there is no crowd service defined as a powershell object class, then we have to build it. We test against url, since that's relatively close to the service object. { try #first we try making a service proxy object from our previously cached descriptor. { $global:crowdService = new-webserviceproxy -uri $wsdlfilenamepath -namespace "Crowd" #new service proxy with a namespace called Crowd, from cached WSDL descriptor stored in file. } catch #if we fail to create it (no cache for example) { try { $message = "Cached object returned from $wsdlfile is an invalid URI descriptor. Attempting to retrieve directly from server. Perhaps the file was manually altered?" #write event message stating that the file returned was bad. Send-EventMessage -eventMessage $message -eventType "Warning" -eventid 38010 -eventlogSource $eventLogSource write-warning $message $global:crowdService = new-webserviceproxy -uri $wsdlurl -namespace "Crowd" #new service proxy with a namespace called Crowd, direct from WSDL descriptor URL, since the attempt to retrieve the cache object from disk failed. if ($global:crowdService) { $message = "WSDL object created from HTTP(S) uri path. Since the disk cached object failed to load, try deleting cache file and try again." #successfully created crowdservice object class. Send-EventMessage -eventMessage $message -eventType "Warning" -eventid 38010 -eventlogSource $eventLogSource write-warning $message } } catch { $message = "FATAL: Could not create WSDL object. Check if WSDL URL specified is operating as expected.`r`n`r`n" + + $error[0] #failed to retrieve directly from url. This is a problem, and we cannot continue. Send-EventMessage -eventMessage $message -eventType "Error" -eventid 38020 -eventlogSource $eventLogSource throw $message } } } else #crowd service object exists, for example it was retrieved in a previous call in this session. { write-verbose ("Crowd Service object for server " + $global:crowdService.url + " exists, and will be re-used.") } if ($global:crowdService.url) #test if the crowd service exists. { ## set up the object with our properties. Note that these aren't global and are discarded once the function exits. ## we only want to set these up if the crowdservice object with classes exists. ## $cachedTokenPath = $wsdlpath + "\" + $wsdlurl.split("/")[2]+".authtoken" #invent a file name for the auth token, unique to the crowd wsdl url we're using. $appAuthentication = new-object Crowd.ApplicationAuthenticationContext #new context for application authentication $appCredential = new-object Crowd.PasswordCredential #new PasswordCredential context $appCredential.credential = $crowdpass #set the credential. Special class - we can't set a credential directly. $appAuthentication.name = $crowdapp #set the name of the crowd application. $appAuthentication.credential = $appCredential #insert the credential object into the application authentication context ## now, we'll try making an authentication attempt to it. ## try loading the previously cached authentication credential. If all goes well, we'll just get the token. If it goes badly, we'll re-load and cache from the server. ## note that we will always test and use the cached version first. Thus, if you have a valid token but don't have a cached version, it will fetch a current token from server ## and then try to cache it. try { $message = "Retrieving cached auth token object from " + $cachedTokenPath write-verbose $message $global:crowdAppToken = AuthTokenSerializer $cachedTokenPath #try and get the previously cached token path. } catch #if it doesn't exist, fail and create a new one, the long slow hard way, from the server. { try { $global:crowdAppToken = $crowdService.authenticateApplication($appAuthentication) #this var is global, so it's available for other functions in the current posh session. } catch { $message = "Failed to authenticate application token on crowd server.`r`n`r`n" + $error[0] Send-EventMessage -eventMessage $message -eventType "Error" -eventid 38020 -eventlogSource $eventLogSource throw $message } #fall through to here if we were able to retrieve an auth token. If we did, go ahead and save it to disk. try { $crowdAppToken | Export-Clixml -force $cachedTokenPath #(over)write the token contents to the file } catch { $message = "Could not write token cache file.`r`n`r`n" + $error[0] write-error $message -category "WriteError" #write an error if we can't create the file. Send-EventMessage -eventMessage $message -eventType "Error" -eventid 38020 -eventlogSource $eventLogSource } } ## this section tests the token and re-loads it if the token is invalid or mangled in some way. ## we've already defined $global:crowdAppToken previously, so assuming it is fine in the test, we won't mess with it and ## at the end of this we can return the object. if ($global:crowdAppToken.name -and $global:crowdAppToken.token) #if the token has a "Name" and "Token" attribute, it's probably at least a properly created token. { if ((test-path $cachedTokenPath) -eq $false)#let's just see if that token is still on the file system and yell at someone if it's not. { $message = "Token object found as session variable, but not as cache object. Ensure your cached tokens are being correctly stored" Send-EventMessage -eventMessage $message -eventType "Warning" -eventid 38010 -eventlogSource $eventLogSource write-warning $message #just a little friendly warning. } ## now, assuming all that we have tried is authenticating and returning a trivial object, we should be able to just glide on through to the end of the function ## returning the $crowdAppToken unmolested. try #try authenticating against the server and returning a trivial object { $null = $crowdservice.getDomain($crowdAppToken) #in this case, the token domain. } catch #if there's an error, it's probably going to be an invalid ticket (or worse). in any case, we'll try again, the slow way. { $message = "Current cached token failed authentication check. Creating and caching a new auth token." Send-EventMessage -eventMessage $message -eventType "Information" -eventid 38000 -eventlogSource $eventLogSource write-verbose $message try { $global:crowdAppToken = $crowdService.authenticateApplication($appAuthentication) #this var is global, so it's available for other functions in the current posh session. } catch { $message = "Failed to authenticate application token on crowd server.`r`n`r`n" + $error[0] Send-EventMessage -eventMessage $message -eventType "Error" -eventid 38020 -eventlogSource $eventLogSource write-error $error[0] -category "PermissionDenied" break } try { write-verbose "Caching new token to file" $crowdAppToken | Export-Clixml -force $cachedTokenPath #(over)write the token contents to the file } catch { $message = "Failed to cache new authentication token to file.`r`n`r`n" + $error[0] Send-EventMessage -eventMessage $message -eventType "Error" -eventid 38020 -eventlogSource $eventLogSource write-error $message -category "WriteError" #write an error if we can't create the file. } } } else { ## this exception is for the state where we've maybe loaded a cli-xml object that is valid, but contains bogus object property data, for example. $message = "Token returned from cache or from authcache refresh session is invalid. Loading and caching new token from server." Send-EventMessage -eventMessage $message -eventType "Warning" -eventid 38010 -eventlogSource $eventLogSource write-warning $message try { $global:crowdAppToken = $crowdService.authenticateApplication($appAuthentication) #this var is global, so it's available for other functions in the current posh session. } catch { $message = "Failed to authenticate application token on crowd server.`r`n`r`n" + $error[0] Send-EventMessage -eventMessage $message -eventType "Error" -eventid 38020 -eventlogSource $eventLogSource throw $message } try { write-verbose "Caching new token to file" $crowdAppToken | Export-Clixml -force $cachedTokenPath #(over)write the token contents to the file } catch { $message = "Failed to cache new authentication token to file.`r`n`r`n" + $error[0] Send-EventMessage -eventMessage $message -eventType "Error" -eventid 38020 -eventlogSource $eventLogSource write-error $message -category "WriteError" #write an error if we can't create the file. } } } else { $message = "crowdService object not correctly instantiated. Check uri URL or cached WSDL object and try again.`r`n`r`n" + $error[0] Send-EventMessage -eventMessage $message -eventType "Error" -eventid 38020 -eventlogSource $eventLogSource write-error $message -category "ObjectNotFound" } return $crowdAppToken #return token from function. } function Crowd-UserAuthenticate {param([Parameter(Mandatory=$true)]$username,$userpass) if (!$crowdService) #if the global object was not previously defined, inform user. { throw "Crowd application not completed. Please try Crowd-ApplicationAuthenticate" } if (!$crowdAppToken) #if a token was not returned during the application authentication, (but a namespace WAS created) inform user. { throw "Crowd application token not defined. Please try Crowd-ApplicationAuthenticate" } if (!$userpass) { $userpass = Read-HostMasked } $userAuthentication = new-object Crowd.UserAuthenticationContext #create a user authentication context $userCredential = new-object Crowd.PasswordCredential #create a user password credential context $userCredential.credential = $userpass #insert password (cleartext) into credential object $userAuthentication.name = $username #store username in user auth context $userAuthentication.application = $crowdAppToken.name #store currently authenticated crowd application name into user auth context. $userAuthentication.credential = $userCredential #store the user credential object in the auth context. return $crowdService.authenticatePrincipal($crowdAppToken,$userAuthentication) #call the crowd service with the crowd application token (global var) and the user auth context, return user auth token. } Function P4AuthTrigger {param([Parameter(Mandatory=$true)]$crowdapp,[Parameter(Mandatory=$true)]$crowdpass,[Parameter(Mandatory=$true)]$username,$userpass) if (!$userpass) { $userpass = Read-HostMasked } $eventLogSource = "Perforce Authentication for Crowd" try { $apptoken = Crowd-ApplicationAuthenticate -crowdapp $crowdapp -crowdpass $crowdpass } catch { write-error ("Could not authenticate application `'" + $crowdapp + "`'. Check your crowd configuration and try again.") -category "PermissionDenied" break } if ($apptoken) { try { $usertoken = Crowd-UserAuthenticate -username $username -userpass $userpass } catch { [console]::error.writeline("User `'" + $username + "`' not authenticated.") break } $message = "Welcome " + $username + " to the " + $crowdapp + " server." $message } else { write-error "Crowd application authentication token not correctly returned. Cannot authenticate user principle." -category "ObjectNotFound" break } } Function AuthTokenSerializer {param([Parameter(Mandatory=$true)]$clixmlfilename) if ($crowdService.url) { $global:crowdAppToken = new-object Crowd.AuthenticatedToken $deserializedAuthTokenObject = import-clixml $clixmlfilename $global:crowdAppToken.name = $deserializedAuthTokenObject.name $global:crowdAppToken.token = $deserializedAuthTokenObject.token } else { write-error "No Crowd Service object available to serialize request" -category "ObjectNotFound" } $global:crowdAppToken } Function Get-HTTP {param([string]$url) $req = [System.Net.WebRequest]::Create($url) $req.Method ="GET" $req.ContentLength = 0 $resp = $req.GetResponse() $reader = new-object System.IO.StreamReader($resp.GetResponseStream()) $reader.ReadToEnd() } ## Resolve-Error # # (note that the out-default is necessary otherwise get an error. works fine from command line. a bug in posh perhaps.) # function Resolve-Error($ErrorRecord=$Error[0]) { $ErrorRecord | fl * -f $ErrorRecord.InvocationInfo | fl * -f $Exception = $ErrorRecord.Exception for ($i = 0; $Exception; $i++, ($Exception = $Exception.InnerException)) { "*$i* " * 15 $Exception | fl * -f } } function Get-CrowdWSDLURL { if (!$env:crowdWSDLurl) #if the system environment variable isn't set { $validXMLMarker = "authentication.integration.crowd.atlassian.com" #we're looking for atlassian crowd integration xml scheme $tries = 0 #set number of tries to configure this to zero $maxtries = 5 #set max number of tries before we just fail. $webcontent = $null #clear this var for testing. write-warning "System environment variable `'crowdWSDLurl`' does not exist. I will attempt to create this for you." $wclient = new-object System.Net.WebClient #create a new object to download stuff. do #do while we're less than $maxtries. Give the user 5 attempts to find the valid wsdl path. after that we give up. { $wsdluserinput = read-host -Prompt "`r`nEnter the full URL or path to the Crowd WSDL.`r`nFor example, https://my-crowd-server.domain.com/crowd/services/?wsdl, or c:\mywsdl.txt" #get input for where we want to get the wsdl. could be url or file. try { $webcontent = $wclient.DownloadString($wsdluserinput) #try to retrieve the object. If we just stuff random crap into xml, we sometimes hang. This is a lame attempt at protecting the script from doing something unpredictable. } catch { write-host ("Specified WSDL url could not be loaded. The error was:`r`n" + $error[0]) #couldn't get the content into a powershell object for some reason. } if ($webcontent)# if webcontent got something that could be stored { if ($webcontent -match "