update Jenkins credentials by script - jenkins

I have a Jenkins server running on Windows. It stores a username:password in the credentials plugin. This is a service user that gets its password updated regularly.
I'm looking for a way to run a script, preferably Powershell, that will update that credential in the Jenkins password store so that it's always up to date when I use it in a build job script.
The password is managed by a Thycotic Secret Server install so I should be able to automate the process of keeping this password up to date, but I have found almost no leads for how to accomplish this, even though the blog post by the guy who wrote the credentials api mentions almost exactly this scenario and then proceeds to just link to the credentials plugin's download page that says nothing about how to actually use the api.
Update
The accepted answer works perfectly, but the rest method call example is using curl, which if you're using windows doesn't help much. Especially if you are trying to invoke the REST URL but your Jenkins server is using AD Integration. To achieve this you can use the following script.
Find the userId and API Token by going to People > User > configure > Show API Token.
$user = "UserID"
$pass = "APIToken"
$pair = "${user}:${pass}"
$bytes = [System.Text.Encoding]::ASCII.GetBytes($pair)
$base64 = [System.Convert]::ToBase64String($bytes)
$basicAuthValue = "Basic $base64"
$headers = #{ Authorization = $basicAuthValue }
Invoke-WebRequest `
-uri "http://YourJenkinsServer:8080/scriptler/run/changeCredentialPassword.groovy?username=UrlEncodedTargetusername&password=URLEncodedNewPassword" `
-method Get `
-Headers $headers

Jenkins supports scripting with the Groovy language. You can get a scripting console by opening in a browser the URL /script of your Jenkins instance. (i.e: http://localhost:8080/script)
The advantage of the Groovy language (over powershell, or anything else) is that those Groovy scripts are executed within Jenkins and have access to everything (config, plugins, jobs, etc).
Then the following code would change the password for user 'BillHurt' to 's3crEt!':
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl
def changePassword = { username, new_password ->
def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class,
Jenkins.instance
)
def c = creds.findResult { it.username == username ? it : null }
if ( c ) {
println "found credential ${c.id} for username ${c.username}"
def credentials_store = Jenkins.instance.getExtensionList(
'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
)[0].getStore()
def result = credentials_store.updateCredentials(
com.cloudbees.plugins.credentials.domains.Domain.global(),
c,
new UsernamePasswordCredentialsImpl(c.scope, c.id, c.description, c.username, new_password)
)
if (result) {
println "password changed for ${username}"
} else {
println "failed to change password for ${username}"
}
} else {
println "could not find credential for ${username}"
}
}
changePassword('BillHurt', 's3crEt!')
Classic automation (/scriptText)
To automate the execution of this script, you can save it to a file (let's say /tmp/changepassword.groovy) and run the following curl command:
curl -d "script=$(cat /tmp/changepassword.groovy)" http://localhost:8080/scriptText
which should respond with a HTTP 200 status and text:
found credential 801cf176-3455-4b6d-a461-457a288fd202 for username BillHurt
password changed for BillHurt
Automation with the Scriptler plugin
You can also install the Jenkins Scriptler plugin and proceed as follow:
Open the Scriptler tool in side menu
fill up the 3 first field taking care to set the Id field to changeCredentialPassword.groovy
check the Define script parameters checkbox
add 2 parameters: username and password
paste the following script:
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl
def changePassword = { username, new_password ->
def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class,
jenkins.model.Jenkins.instance
)
def c = creds.findResult { it.username == username ? it : null }
if ( c ) {
println "found credential ${c.id} for username ${c.username}"
def credentials_store = jenkins.model.Jenkins.instance.getExtensionList(
'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
)[0].getStore()
def result = credentials_store.updateCredentials(
com.cloudbees.plugins.credentials.domains.Domain.global(),
c,
new UsernamePasswordCredentialsImpl(c.scope, null, c.description, c.username, new_password)
)
if (result) {
println "password changed for ${username}"
} else {
println "failed to change password for ${username}"
}
} else {
println "could not find credential for ${username}"
}
}
changePassword("$username", "$password")
and click the Submit button
Now you can call the following URL to change the password (replacing the username and password parameter): http://localhost:8080/scriptler/run/changeCredentialPassword.groovy?username=BillHurt&password=s3crEt%21 (notice the need to urlencode the parameters' value)
or with curl:
curl -G http://localhost:8080/scriptler/run/changeCredentialPassword.groovy --data-urlencode 'username=BillHurt' --data-urlencode "password=s3crEt!"
sources:
Printing a list of credentials and their IDs
Create UserPrivateKeySource Credential via Groovy?
credential plugin source code
Scriptler plugin
Search engine tip: use keywords 'Jenkins.instance.', 'com.cloudbees.plugins.credentials' and UsernamePasswordCredentialsImpl

Decided to write a new answer although it is basically some update to #Tomasleveil's answer:
removing deprecated calls (thanks to jenkins wiki, for other listing options see plugin consumer guide)
adding some comments
preserve credentials ID to avoid breaking existing jobs
lookup credentials by description because usernames are rarely so unique (reader can easily change this to ID lookup)
Here it goes:
credentialsDescription = "my credentials description"
newPassword = "hello"
// list credentials
def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
// com.cloudbees.plugins.credentials.common.StandardUsernameCredentials to catch all types
com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials.class,
Jenkins.instance,
null,
null
);
// select based on description (based on ID might be even better)
cred = creds.find { it.description == credentialsDescription}
println "current values: ${cred.username}:${cred.password} / ${cred.id}"
// not sure what the other stores would be useful for, but you can list more stores by
// com.cloudbees.plugins.credentials.CredentialsProvider.all()
credentials_store = jenkins.model.Jenkins.instance.getExtensionList(
'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
)[0].getStore()
// replace existing credentials with a new instance
updated = credentials_store.updateCredentials(
com.cloudbees.plugins.credentials.domains.Domain.global(),
cred,
// make sure you create an instance from the correct type
new com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl(cred.scope, cred.id, cred.description, cred.username, newPassword)
)
if (updated) {
println "password changed for '${cred.description}'"
} else {
println "failed to change password for '${cred.description}'"
}

I never found a way to get the Groovy script to stop updating the credential ID, but I noticed that if I used the web interface to update credentials, the ID did not change.
With that in mind the script below will in effect script the Jenkins web interface to do the updates.
Just for clarification, the reason this is important is because if you are using something like the Credentials binding plugin to use credentials in your job, updating the ID to the credential will break that link and your job will fail. Please excuse the use of $args rather than param() as this is just an ugly hack that I will refine later.
Note the addition of the json payload to the form fields. I found out the hard way that this is required in this very specific format or the form submission will fail.
$username = $args[0]
$password = $args[1]
Add-Type -AssemblyName System.Web
#1. Log in and capture the session.
$homepageResponse = Invoke-WebRequest -uri http://Servername.domain.com/login?from=%2F -SessionVariable session
$loginForm = $homepageResponse.Forms['login']
$loginForm.Fields.j_username = $username
$loginForm.Fields.j_password = $password
$loginResponse = Invoke-WebRequest `
-Uri http://Servername.domain.com/j_acegi_security_check `
-Method Post `
-Body $loginForm.Fields `
-WebSession $session
#2. Get Credential ID
$uri = "http://Servername.domain.com/credential-store/domain/_/api/xml"
foreach($id in [string]((([xml](Invoke-WebRequest -uri $uri -method Get -Headers $headers -WebSession $session).content)).domainWrapper.Credentials | Get-Member -MemberType Property).Name -split ' '){
$id = $id -replace '_',''
$uri = "http://Servername.domain.com/credential-store/domain/_/credential/$id/api/xml"
$displayName = ([xml](Invoke-WebRequest -uri $uri -method Get -Headers $headers -WebSession $session).content).credentialsWrapper.displayName
if($displayName -match $username){
$credentialID = $id
}
}
#3. Get Update Form
$updatePage = Invoke-WebRequest -Uri "http://Servername.domain.com/credential-store/domain/_/credential/$credentialID/update" -WebSession $session
$updateForm = $updatePage.Forms['update']
$updateForm.Fields.'_.password' = $password
#4. Submit Update Form
$json = #{"stapler-class" = "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl";
"scope"="GLOBAL";"username"="domain\$username";"password"=$password;"description"="";"id"=$id} | ConvertTo-Json
$updateForm.Fields.Add("json",$json)
Invoke-WebRequest `
-Uri "http://Servername.domain.com/credential-store/domain/_/credential/$credentialID/updateSubmit" `
-Method Post `
-Body $updateForm.Fields `
-WebSession $session

Related

Is it possible to use a variable in the Agent.Name demand for tfs?

I am trying to force the run of a specific agent phase on a specific agent. My variable however does not seems to be picked up. I get the error:
No agent found in pool TEST which satisfies the specified demands:
Agent.Name -equals $(Release.ReleaseName)
vstest
Agent.Version -gtVersion 2.103.0
Is this possible?
I wanted to do something similar. I had 2 "environments" in the TFS release definition. The first one ("MyEnvironment-Setup") did setup like big file copies and database script generation. The second ("MyEnvironment") did the actual deployment. I needed them both to run on the same agent.
What I ended up doing is using the TFS API during "MyEnvironment-Setup" to modify the currently running release instance and set the agent demand for "MyEnvironment".
Several things were necessary to make this happen.
In "MyEnvironment-Setup" > Run on Agent > Additional Options, I enabled "Allow scripts to access the OAuth token"
In the left pane of the release definition screen where it lists all release definitions, I clicked the ellipsis next to "All release definitions" and selected "Security". Then for the "Project Collection Build Service (TEAM FOUNDATION)" user, I set "Edit release definition" and "Manage releases" to "Allow".
I called the following Powershell method in "MyEnvironment-Setup"
function Set-FinalAgent()
{
# Force the "MyEnvironment" step to run on the same agent as the "MyEnvironment-Setup" step
$baseApiUrl = "$($env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI)$env:SYSTEM_TEAMPROJECTID/_apis"
$apiVersion = "3.1-preview"
$header = #{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" }
$environmentToModify = $($env:Release_EnvironmentName).Replace("-Setup", "")
$releaseID = $env:Release_ReleaseID
$release = Invoke-RestMethod -Uri "$baseApiUrl/release/releases/$($releaseID)?api-version=$apiVersion" -Method GET -Header $header
($release.environments | where name -eq $environmentToModify).deployPhasesSnapshot[0].deploymentInput.demands += "Agent.Name -equals $($env:Agent_Name)"
# Compression is needed for the body to contain all of the info for larger release definitions when running on older servers
$body = $release | ConvertTo-Json -Depth 100 -Compress
Invoke-RestMethod -Uri "$baseApiUrl/release/releases/$($releaseID)?api-version=$apiVersion" -Method PUT -Header $header -ContentType application/json -Body $body
}
If you want to test and tweak this in Powershell ISE or something where you don't have access to the OAuth token, you can use a personal access token instead. In the top-right of your TFS Web UI, click your profile picture and select "Security" to generate a PAT. Then you can replace the header in the script above with the following:
$header = #{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($personalAccessToken)"))}

How to Login to VSTS programmatically

I have an automatic branching utility that does various operations.
We have upgraded to VSTS and I am having trouble using the Tfs calls with the new server:
//login
NetworkCredential netCred = new NetworkCredential("emailAddress", password");
BasicAuthCredential basicCred = new BasicAuthCredential(netCred);
TfsClientCredentials tfsCred = new TfsClientCredentials(basicCred);
tfsCred.AllowInteractive = false;
TfsTeamProjectCollection tfsServer = new TfsTeamProjectCollection(new Uri("https://myco.visualstudio.com/mycoll"), tfsCred);
// get a local workspace
VersionControlServer sc = server.GetService(typeof(VersionControlServer)) as VersionControlServer;
... other code
and then boom!
"You are not authorized to access https://myco.visualstudio.com/mycoll"
Is there some setting somewhere?
Should I try and use the REST API?
Am I calling something I shouldn't?
I've tried all sorts of formats for the URI, with :8080, with /tfs in the path to no avail!
You can use PAT (Personal Access Token) to access VSTS. Click on your profile in VSTS and go to security menu item. You can define a PAT in there. Keep the PAT saved as it will only be shown once.
Define the scope as per your needs
You can use the PAT to access TFS REST API as shown in following PowerShell sample. Using https://yourAccount.visualstudio.com or https://yourAccount.visualstudio.com/DefaultCollection for ColletionUri parameter is OK.
param(
[Parameter(Mandatory=$true)]
[string] $token,
[Parameter(Mandatory=$true)]
[string] $collectionUri
)
$User=""
# Base64-encodes the Personal Access Token (PAT) appropriately
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $User,$token)));
$header = #{Authorization=("Basic {0}" -f $base64AuthInfo)};
$Uri = $collectionUri + '/_apis/projects?api-version=1.0'
$projects = Invoke-RestMethod -Method Get -ContentType application/json -Uri $Uri -Headers $header
You can find C# code samples here https://www.domstamand.com/accessing-tfs-2017-programmatically/

Redeployment with Powershell in Multi Tenant Environment

I want to redeploy a multiple tenants using powershell. We have 100+ tenants in UIT and I don't want to do this manually. In order to redeploy, I have to use a specific link and log in. My question is, if it is possible to do this in an automated way using powershell. Here is what I have so far:
Function SendRequest($tenantName)
{
$url = "http://<machine-name>/$tenantName/Account /LogOn?ReturnUrl=%2f$tenantName%2fadmin%2fdeploytenant%3fsyncmetadata%3dtrue&syncmetadata=true"
$req = [system.Net.WebRequest]::Create($url)
$req.Credentials = [System.Net.NetworkCredential]::($username, (ConvertTo-SecureString $pwd -AsPlainText -force))
try
{
$res = $req.GetResponse()
}
catch [System.Net.WebException]
{
$res = $_.Exception.Response
}
$int = [int]$res.StatusCode
$status = $res.StatusCode
Write-Host $status
return "$int $status"
}
The status that is returned is "200 OK" but it does not redeploy. Maybe I need to log in first and then send another request with the redeployment parameters. So what would be the best way to accomplish this?
Maybe this is just a snippet of your code, but I'm seeing you reference $username and $pwd without actually feeding in a username and password.
Perhaps that could that explain the lack of a successful redeploy.

Powershell EWS Send Email from Shared Mailbox

I am using Powershell and the Exchange Web Service (v1.2) to send an email through Office 365. As long as I use my credentials the script is able to send without an issue. However, for tracking purposes, on script errors I need to send the email from a shared mailbox instead of through my account. The error I get is:
Exception calling "AutodiscoverUrl" with "2" argument(s): "The Autodiscover service couldn't be located."
At F:\Scripting\1-Dev\Modules\Include.ps1:387 char:31
+ $service.AutodiscoverUrl <<<< ($Credential.UserName, {$true})
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException
I'm sure the problem is authentication. How can I authenticate and send from a shared mailbox in o365?
EDIT:
Here is the code that I'm using to send the email:
Function Send-O365Email {
[CmdletBinding()]
param(
[Parameter(Position=1, Mandatory=$true)]
[String[]] $To,
[Parameter(Position=2, Mandatory=$true)]
[String] $Subject,
[Parameter(Position=3, Mandatory=$true)]
[String] $Body,
[Parameter(Position=4, Mandatory=$true)]
[System.Management.Automation.PSCredential] $Credential,
[Parameter(Mandatory=$false)]
[String]$PathToAttachment
)
begin {
#Load the EWS Managed API Assembly
Add-Type -Path 'C:\Program Files\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll'
}
process {
#Create the EWS service object
$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService -ArgumentList Exchange2010_SP1
#Set the credentials for Exchange Online
$service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials -ArgumentList `
$Credential.UserName, $Credential.GetNetworkCredential().Password
#Determine the EWS endpoint using autodiscover
$service.AutodiscoverUrl($Credential.UserName, {$true})
#Create the email message and set the Subject and Body
$message = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage -ArgumentList $service
$message.Subject = $Subject
$message.Body = $Body
$message.Body.BodyType = 'HTML'
if (Test-Path $Attachment) {
$message.Attachments.AddFileAttachment("$Attachment");
}
#Add each specified recipient
$To | ForEach-Object {
$null = $message.ToRecipients.Add($_)
}
#Send the message and save a copy in the users Sent Items folder
try {
$message.SendAndSaveCopy()
}
catch [exception] {
Add-Content -Path $LOGFILE -Value "$_"
}
} # End Process
}
You may want to try the 2.0 assembly, though I don't think that is the issue http://www.microsoft.com/en-us/download/details.aspx?id=35371
My environment is a little different, in that our credentials and email address are not the same name. (one is initialLastName#Comp.com and the other is first.Last#Comp.com) so I have to be a little different in how I approach names, always needing a Credential and a MailBox name.
When I try to use a Resource account, I have to use my email for the autodiscovery. Possibly because the Service account doesn't have a user license. I am speculating.
I found a good amount of resources at http://gsexdev.blogspot.com/.
Specifically, this may be what you need:
## Optional section for Exchange Impersonation
# http://msdn.microsoft.com/en-us/library/exchange/dd633680(v=exchg.80).aspx
$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, "SupportResource#comp.com")
#or clear with
#$service.ImpersonatedUserId = $null
similar 'discussion' # EWS Managed API: how to set From of email? also.

Use Twitteroauth to acquire twitter user info

I use the following code to set some sessions when an user logs in using his twitter account:
$twitteroauth = new TwitterOAuth(YOUR_CONSUMER_KEY, YOUR_CONSUMER_SECRET, $_SESSION['oauth_token'], $_SESSION['oauth_token_secret']);
$access_token = $twitteroauth->getAccessToken($_GET['oauth_verifier']);
$_SESSION['access_token'] = $access_token;
$user_info = $twitteroauth->get('account/verify_credentials');
As you see, I have all the tokens (token, secret token and access token) stored in a session, so that I can use this later on when an user wants to change his profile picture for example
But when I want to have access again to the user info... I am not able to access it. Again I build the twitteroauth connection, but now using the sessions stored during login:
$twitteroauth = new TwitterOAuth(YOUR_CONSUMER_KEY, YOUR_CONSUMER_SECRET, $_SESSION['oauth_token'], $_SESSION['oauth_token_secret']);
$access_token = $_SESSION['access_token'];
$user_info = $twitteroauth->get('account/verify_credentials');
if (isset($user_info->error)) {
echo "token:", $_SESSION['oauth_token'], "<br>";
echo "token_secret:", $_SESSION['oauth_token_secret'], "<br>";
echo "access-token:", $access_token, "<br>";
echo "error";
} else {
echo "oke";
}
I receive the error echo. When I echo my token strings, the all contain data and the access-token contains the value "Array".
Does someone know what I am doing wrong?
You need to use print_r to print the array values.

Resources