15

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
Bill Hurt
  • 749
  • 1
  • 8
  • 26

3 Answers3

27

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:

enter image description here

  • Open the Scriptler tool in side menu

enter image description here

  • 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:

Search engine tip: use keywords 'Jenkins.instance.', 'com.cloudbees.plugins.credentials' and UsernamePasswordCredentialsImpl

akostadinov
  • 17,364
  • 6
  • 77
  • 85
Thomasleveil
  • 95,867
  • 15
  • 119
  • 113
  • I like the scriptler plugin approach very much. That should be very easy to translate into a Powershell call to Invoke-WebRequest from the secret server. Thanks very much. As soon as I get this tested I will mark your solution as the answer. – Bill Hurt Aug 26 '15 at 15:47
  • @BillHurt any positive outcome? – Thomasleveil Sep 05 '15 at 18:12
  • Sorry not yet. This project got de-prioritized for a bit. I hope to test within the next couple weeks. – Bill Hurt Sep 08 '15 at 13:13
  • I'm marking this as the answer because I tested today and the script works. I'm using AD authentication with this server so I need to find a way to authenticate before I can make the remote REST call, but that's a separate issue. Your suggestion works perfectly. Thanks very much. – Bill Hurt Sep 11 '15 at 14:44
  • Oh, I forgot. I did have to add an additional import at the top of the script: import jenkins.model.Jenkins – Bill Hurt Sep 11 '15 at 15:33
  • I've found an issue with the script. In the arguments to credentials_store.updateCredentials( "new UsernamePasswordCredentialsImpl" doesn't seem so much to update the credentials as to create a new one with the same name. When my jobs reference the credentials for use, they reference in the background by ID. When this script runs the creds' ID is changed and my jobs link to the credentials is broken. Any thoughts? – Bill Hurt Jan 06 '16 at 16:23
  • this script assumes the credentials are of type `StandardUsernameCredentials`, if yours is different, the script has to be adapted – Thomasleveil Jan 06 '16 at 16:34
  • I'm just using the standard Jenkins Credentials Store with Global scope so that the jobs can see them. I'm not sure how to determine what type my credentials are or how to fix it if they are wrong. – Bill Hurt Jan 06 '16 at 16:45
  • I have no clue. Maybe try to reach Jenkins developers on their mailing list: https://groups.google.com/forum/#!forum/jenkinsci-dev or IRC channels: https://wiki.jenkins-ci.org/display/JENKINS/IRC+Channel – Thomasleveil Jan 06 '16 at 19:28
  • I'll do that. Thanks for taking a look at an old answer. – Bill Hurt Jan 06 '16 at 20:06
5

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}'"
}
akostadinov
  • 17,364
  • 6
  • 77
  • 85
1

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 
Bill Hurt
  • 749
  • 1
  • 8
  • 26
  • What language is this? PHP? – Brandon Aug 10 '17 at 19:28
  • 1
    @Brandon That script is PowerShell. It simulates a log in to the regular web interface because there is no supported REST api method for doing this. Or at least there wasn't at the time I wrote it. – Bill Hurt Aug 11 '17 at 20:23
  • 1
    In @Tomasleveil's answer, where you are creating the new credentials `new UsernamePasswordCredentialsImpl`, instead of `null` you can put there `c.getId()` and this will retain the original ID of the credentials. Much preferable compared to using REST or web scraping. – akostadinov Feb 07 '18 at 20:53