-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from kentrikos/feature/credential-mover
This enables migration of credentials between Jenkins instances. Butler will get the XML config of any folder and will ingest the encrypted secrets. The encrypted secrets are then decrypted via a Groovy script executed on Jenkins master. This needs most of the times admin privileges. In a second step you can pipe in the decrypted credentials and apply them to another folder or even instance. Also a Groovy script is leveraged in order to achieve this.
- Loading branch information
Showing
6 changed files
with
406 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"log" | ||
"os" | ||
) | ||
|
||
func DecryptFolderCredentials(url string, folderName string, username string, password string) error { | ||
folder, _ := GetFolder(url, folderName, username, password) | ||
credentials := folder.GetCredentials() | ||
script := GetDecryptScriptForCredentials(credentials) | ||
response := ExecuteGroovyScriptOnJenkins(script, url, username, password) | ||
fmt.Println(response) | ||
return nil | ||
} | ||
|
||
func ApplyFolderCredentials(url string, folderName string, username string, password string) error { | ||
var credentials Credentials | ||
|
||
err := json.NewDecoder(os.Stdin).Decode(&credentials) | ||
if err != nil { | ||
log.Fatal(err) | ||
panic(err) | ||
} | ||
script := GetApplyScriptForCredentials(credentials, folderName) | ||
response := ExecuteGroovyScriptOnJenkins(script, url, username, password) | ||
fmt.Println(response) | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,10 @@ | ||
package main | ||
|
||
import "testing" | ||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestGetFolderURL(t *testing.T) { | ||
type args struct { | ||
|
@@ -69,3 +73,61 @@ func TestGetFolderURL(t *testing.T) { | |
}) | ||
} | ||
} | ||
|
||
func TestParseJenkinsFolder(t *testing.T) { | ||
assert := assert.New(t) | ||
xml := `<?xml version='1.1' encoding='UTF-8'?> | ||
<com.cloudbees.hudson.plugins.folder.Folder plugin="[email protected]"> | ||
<actions/> | ||
<displayName>Ansible</displayName> | ||
<properties> | ||
<com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider_-FolderCredentialsProperty> | ||
<domainCredentialsMap class="hudson.util.CopyOnWriteMap$Hash"> | ||
<entry> | ||
<com.cloudbees.plugins.credentials.domains.Domain plugin="[email protected]"> | ||
<specifications/> | ||
</com.cloudbees.plugins.credentials.domains.Domain> | ||
<java.util.concurrent.CopyOnWriteArrayList> | ||
<com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl plugin="[email protected]"> | ||
<id>test</id> | ||
<description>Test</description> | ||
<username>Test</username> | ||
<password>{AQAAABAAAAAQ7EzV5N/fXZEKM9HyG+1T66P67iqU+tptVCNuvNX1TM0=}</password> | ||
</com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl> | ||
<org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl plugin="[email protected]"> | ||
<id>deploy-key-file</id> | ||
<description>blub</description> | ||
<fileName>accessKeys.csv</fileName> | ||
<secretBytes>{bEtRRJ+hCoQHgEAmcGhAOlKFx6J5tVuKmwdBVSgdq4zkktsLwG1zHO6swI3mQ5z9UhbgRRHDf2W8oSHlfmno8+KHWKWKyNmQUL5cv6/8n5JnmvsMGx+DT4KJL2XDVl33nuNbDpkcJEDGBWqb2hA47iRtW6h4mxlbNja5E12eUMs=}</secretBytes> | ||
</org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl> | ||
</java.util.concurrent.CopyOnWriteArrayList> | ||
</entry> | ||
</domainCredentialsMap> | ||
</com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider_-FolderCredentialsProperty> | ||
</properties> | ||
<folderViews class="com.cloudbees.hudson.plugins.folder.views.DefaultFolderViewHolder"> | ||
<views> | ||
<hudson.model.AllView> | ||
<owner class="com.cloudbees.hudson.plugins.folder.Folder" reference="../../../.."/> | ||
<name>all</name> | ||
<filterExecutors>false</filterExecutors> | ||
<filterQueue>false</filterQueue> | ||
<properties class="hudson.model.View$PropertyList"/> | ||
</hudson.model.AllView> | ||
</views> | ||
<primaryView>all</primaryView> | ||
<tabBar class="hudson.views.DefaultViewsTabBar"/> | ||
</folderViews> | ||
<healthMetrics> | ||
<com.cloudbees.hudson.plugins.folder.health.WorstChildHealthMetric> | ||
<nonRecursive>false</nonRecursive> | ||
</com.cloudbees.hudson.plugins.folder.health.WorstChildHealthMetric> | ||
</healthMetrics> | ||
<icon class="com.cloudbees.hudson.plugins.folder.icons.StockFolderIcon"/> | ||
</com.cloudbees.hudson.plugins.folder.Folder>` | ||
|
||
got := parseJenkinsFolder([]byte(xml)) | ||
|
||
assert.NotNil(got, "Should not be nil.") | ||
assert.Equal(got.GetCredentials().UsernamePassword[0].ID, "test") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
) | ||
|
||
func GetDecryptScriptForCredentials(credentials Credentials) string { | ||
marshalledCredentials, _ := json.Marshal(credentials) | ||
return strings.Replace(decryptScriptTemplate, "<<JSON HERE>>", string(marshalledCredentials), 1) | ||
} | ||
|
||
const decryptScriptTemplate = `import groovy.json.JsonSlurperClassic | ||
import groovy.json.JsonOutput | ||
def json = """<<JSON HERE>>""" | ||
def data = new JsonSlurperClassic().parseText(json) | ||
data.userpass.each { | ||
it.password = hudson.util.Secret.fromString(it.password).getPlainText() | ||
} | ||
data.secretfile.each { | ||
it.secretBytes = new String(com.cloudbees.plugins.credentials.SecretBytes.fromString(it.secretBytes).getPlainData(), "ASCII") | ||
} | ||
println JsonOutput.toJson(data)` | ||
|
||
func GetApplyScriptForCredentials(credentials Credentials, folderPath string) string { | ||
marshalledCredentials, _ := json.Marshal(credentials) | ||
templated := strings.Replace(createOrUpdateCredentialsTemplate, "<<JSON HERE>>", string(marshalledCredentials), 1) | ||
templated = strings.Replace(templated, "<<FOLDER HERE>>", folderPath, 1) | ||
return templated | ||
} | ||
|
||
const createOrUpdateCredentialsTemplate = `import com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider.FolderCredentialsProperty | ||
import com.cloudbees.hudson.plugins.folder.AbstractFolder | ||
import com.cloudbees.hudson.plugins.folder.Folder | ||
import jenkins.model.* | ||
import com.cloudbees.plugins.credentials.* | ||
import com.cloudbees.plugins.credentials.common.* | ||
import com.cloudbees.plugins.credentials.domains.* | ||
import com.cloudbees.plugins.credentials.impl.* | ||
import org.jenkinsci.plugins.plaincredentials.* | ||
import org.jenkinsci.plugins.plaincredentials.impl.* | ||
import org.apache.commons.fileupload.FileItem | ||
import groovy.json.JsonSlurperClassic | ||
import groovy.json.JsonOutput | ||
def createOrUpdateCredential(credentialStore, newCredential, existingCredentials) { | ||
def existingCredential = existingCredentials.find{c -> c.getId() == newCredential.getId()} | ||
if (existingCredential) | ||
credentialStore.updateCredentials(Domain.global(), existingCredential, newCredential) | ||
else | ||
credentialStore.addCredentials(Domain.global(), newCredential) | ||
} | ||
def json = """<<JSON HERE>>""" | ||
String folderPath = "<<FOLDER HERE>>" | ||
def data = new JsonSlurperClassic().parseText(json) | ||
Jenkins.instance.getAllItems(Folder.class) | ||
.findAll{it.fullName.equals(folderPath)} | ||
.each{ | ||
AbstractFolder<?> folderAbs = AbstractFolder.class.cast(it) | ||
println "We're at ${folderAbs.fullName}" | ||
FolderCredentialsProperty property = folderAbs.getProperties().get(FolderCredentialsProperty.class) | ||
if(property == null){ | ||
property = new FolderCredentialsProperty() | ||
folderAbs.addProperty(property) | ||
} | ||
def store = property.getStore() | ||
def existingCredentials = property.getCredentials() | ||
data.userpass.each { | ||
Credentials c = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, it.id, it.description, it.username, it.password) | ||
createOrUpdateCredential(store, c, existingCredentials) | ||
} | ||
data.secretfile.each { | ||
def rawSecretFile = it | ||
fileItem = [ getName: { return rawSecretFile.fileName}, get: { return rawSecretFile.secretBytes.getBytes() } ] as FileItem | ||
secretFile = new FileCredentialsImpl( | ||
CredentialsScope.GLOBAL, | ||
rawSecretFile.id, | ||
rawSecretFile.description, | ||
fileItem, // Don't use FileItem | ||
null, | ||
"") | ||
createOrUpdateCredential(store, secretFile, existingCredentials) | ||
} | ||
println existingCredentials.toString() | ||
}` | ||
|
||
func ExecuteGroovyScriptOnJenkins(script string, rawUrl string, username string, password string) string { | ||
apiURL := fmt.Sprintf("%s/scriptText", rawUrl) | ||
data := url.Values{} | ||
data.Set("script", script) | ||
body := strings.NewReader(data.Encode()) | ||
req, err := http.NewRequest("POST", apiURL, body) | ||
if err != nil { | ||
panic(err) | ||
} | ||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||
|
||
crumb, err := GetCrumb(rawUrl, username, password) | ||
if err != nil { | ||
fmt.Errorf("No crumb issueing possible: %v", err) | ||
} else { | ||
req.Header.Set(crumb[0], crumb[1]) | ||
} | ||
|
||
req.SetBasicAuth(username, password) | ||
resp, err := http.DefaultClient.Do(req) | ||
if err != nil { | ||
panic(err) | ||
} | ||
defer resp.Body.Close() | ||
responseBody, _ := ioutil.ReadAll(resp.Body) | ||
return string(responseBody) | ||
} |
Oops, something went wrong.