First version of jenkins credentials enforcing via script API.
diff --git a/_modules/jenkins_common.py b/_modules/jenkins_common.py
new file mode 100644
index 0000000..00698b2
--- /dev/null
+++ b/_modules/jenkins_common.py
@@ -0,0 +1,106 @@
+import bcrypt
+import logging
+import requests
+from salt.exceptions import SaltInvocationError
+
+logger = logging.getLogger(__name__)
+
+
+def call_groovy_script(script, props):
+ """
+ Common method for call Jenkins groovy script API
+
+ :param script groovy script template
+ :param props groovy script properties
+ :returns: HTTP dict {status,code,msg}
+ """
+ ret = {
+ "status": "FAILED",
+ "code": 999,
+ "msg": ""
+ }
+ jenkins_url, jenkins_user, jenkins_password = get_jenkins_auth()
+ if not jenkins_url:
+ raise SaltInvocationError('No Jenkins URL found.')
+ tokenObj = get_api_crumb(jenkins_url, jenkins_user, jenkins_password)
+ if tokenObj:
+ logger.debug("Calling Jenkins script API with URL: %s",jenkins_url)
+ req = requests.post('%s/scriptText' % jenkins_url,
+ auth=(jenkins_user, jenkins_password),
+ data={tokenObj["crumbRequestField"]: tokenObj["crumb"],
+ "script": render_groovy_script(script, props)})
+ ret["code"] = req.status_code
+ if req.status_code == 200:
+ ret["status"] = "SUCCESS"
+ logger.debug("Jenkins script API call success")
+ ret["msg"] = req.text
+ else:
+ logger.error("Jenkins script API call failed. \
+ Return code %s. Text: %s", req.status_code,req.text)
+ else:
+ logger.error("Cannot call Jenkins script API, Token is invalid!")
+ return ret
+
+
+def render_groovy_script(script, props):
+ """
+ Helper method for rendering groovy script with props
+
+ :param name: groovy script tempalte
+ :param scope: groovy script properties
+ :returns: generated groovy script
+ """
+ return script.format(**props)
+
+
+def get_api_crumb(jenkins_url=None, jenkins_user=None, jenkins_password=None):
+ """
+ Obtains Jenkins API crumb, if CSRF protection is enabled.
+ Jenkins params can be given by params or not, if not,
+ params will be get from salt.
+
+ :param jenkins_url: Jenkins URL (optional)
+ :param jenkins_user: Jenkins admin username (optional)
+ :param jenkins_password: Jenkins admin password (optional)
+ :returns: salt-specified state dict
+ """
+ if not jenkins_url:
+ jenkins_url, jenkins_user, jenkins_password = get_jenkins_auth()
+ logger.debug("Obtaining Jenkins API crumb for URL: %s", jenkins_url)
+ tokenReq = requests.get("%s/crumbIssuer/api/json" % jenkins_url,
+ auth=(jenkins_user, jenkins_password) if jenkins_user else None)
+ if tokenReq.status_code == 200:
+ return tokenReq.json()
+ else:
+ logger.error("Cannot obtain Jenkins API crumb. Status code: %s. Text: %s",
+ tokenReq.status_code,tokenReq.text)
+
+
+
+def get_jenkins_auth():
+ """
+ Get jenkins params from salt
+ """
+ jenkins_url = __salt__['config.get']('jenkins.url') or \
+ __salt__['config.get']('jenkins:url') or \
+ __salt__['pillar.get']('jenkins.url')
+
+ jenkins_user = __salt__['config.get']('jenkins.user') or \
+ __salt__['config.get']('jenkins:user') or \
+ __salt__['pillar.get']('jenkins.user')
+
+ jenkins_password = __salt__['config.get']('jenkins.password') or \
+ __salt__['config.get']('jenkins:password') or \
+ __salt__['pillar.get']('jenkins.password')
+
+ return (jenkins_url, jenkins_user, jenkins_password)
+
+
+def encode_password(password):
+ """
+ Hash plaintext password by jenkins bcrypt algorithm
+ :param password: plain-text password
+ :returns: bcrypt hashed password
+ """
+ if isinstance(password, str):
+ return bcrypt.hashpw(password, bcrypt.gensalt(prefix=b"2a"))
diff --git a/_modules/jenkins_hash.py b/_modules/jenkins_hash.py
deleted file mode 100644
index cf9a9d6..0000000
--- a/_modules/jenkins_hash.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import bcrypt
-
-
-def encode_password(password):
- if isinstance(password, str):
- return bcrypt.hashpw(password, bcrypt.gensalt(prefix=b"2a"))
diff --git a/_states/jenkins_credentials.py b/_states/jenkins_credentials.py
new file mode 100644
index 0000000..355edde
--- /dev/null
+++ b/_states/jenkins_credentials.py
@@ -0,0 +1,93 @@
+import logging
+logger = logging.getLogger(__name__)
+
+create_credential_groovy = u"""\
+import jenkins.*;
+import jenkins.model.*;
+import hudson.*;
+import hudson.model.*;
+
+import com.cloudbees.plugins.credentials.domains.Domain;
+import com.cloudbees.plugins.credentials.CredentialsScope;
+
+domain = Domain.global()
+store = Jenkins.instance.getExtensionList(
+ 'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
+)[0].getStore()
+
+credentials_new = new {clazz}(
+ {params}
+)
+
+creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
+ {clazz}.class, Jenkins.instance
+);
+updated = false;
+
+for (credentials_current in creds) {{
+ // Comparison does not compare passwords but identity.
+ if (credentials_new == credentials_current) {{
+ store.removeCredentials(domain, credentials_current);
+ ret = store.addCredentials(domain, credentials_new)
+ updated = true;
+ println("OVERWRITTEN");
+ break;
+ }}
+}}
+
+if (!updated) {{
+ ret = store.addCredentials(domain, credentials_new)
+ if (ret) {{
+ println("CREATED");
+ }} else {{
+ println("FAILED");
+ }}
+}}
+""" # noqa
+
+
+def present(name, scope, username, password=None, desc="", key=None):
+ """
+ Main jenkins credentials state method
+
+ :param name: credential name
+ :param scope: credential scope
+ :param username: username
+ :param password: password (optional)
+ :param desc: credential description (optional)
+ :param key: credential key (optional)
+ :returns: salt-specified state dict
+ """
+ test = __opts__['test'] # noqa
+ ret = {
+ 'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': '',
+ }
+ result = False
+ if test:
+ status = 'CREATED'
+ ret['changes'][name] = status
+ ret['comment'] = 'Credentials ' + status.lower()
+ else:
+ clazz = ""
+ if key:
+ clazz = "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey"
+ params = 'CredentialsScope.{}, "{}", "{}", "{}"'.format(scope, name, desc, key)
+ else:
+ clazz = "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl"
+ params = 'CredentialsScope.{}, "{}", "{}", "{}", "{}"'.format(scope, name, desc, username, password)
+
+ call_result = __salt__['jenkins_common.call_groovy_script'](create_credential_groovy, {"clazz": clazz, "params":params})
+ if call_result["code"] == 200 and call_result["msg"].strip() in ["CREATED", "OVERWRITTEN"]:
+ status = call_result["msg"]
+ ret['changes'][name] = status
+ ret['comment'] = 'Credentials ' + status.lower()
+ result = True
+ else:
+ status = 'FAILED'
+ logger.error("Jenkins script API execution failure: %s", call_result["msg"])
+ ret['comment'] = 'Jenkins script API execution failure: %s' % (call_result["msg"])
+ ret['result'] = None if test else result
+ return ret
diff --git a/jenkins/client/credential.sls b/jenkins/client/credential.sls
new file mode 100644
index 0000000..4d5412e
--- /dev/null
+++ b/jenkins/client/credential.sls
@@ -0,0 +1,13 @@
+{% from "jenkins/map.jinja" import client with context %}
+{% for name, cred in client.get('credential',{}).iteritems() %}
+credential_{{ name }}:
+ jenkins_credentials.present:
+ - name: {{ cred.get('name', name) }}
+ - username: {{ cred.username }}
+ - password: {{ cred.get('password', '') }}
+ - desc: {{ cred.get('desc', '') }}
+ - scope: {{ cred.get('scope','GLOBAL') }}
+ {%- if cred.key is defined %}
+ - key: {{ cred.get('key','') }}
+ {%- endif %}
+{% endfor %}
\ No newline at end of file
diff --git a/jenkins/client/init.sls b/jenkins/client/init.sls
index 4a9fdc8..c89954a 100644
--- a/jenkins/client/init.sls
+++ b/jenkins/client/init.sls
@@ -4,7 +4,7 @@
include:
- jenkins.client.source
- jenkins.client.job
-
+ - jenkins.client.credential
jenkins_client_install:
pkg.installed:
- names: {{ client.pkgs }}
diff --git a/jenkins/files/config.xml.user b/jenkins/files/config.xml.user
index b28a87f..67c09b9 100644
--- a/jenkins/files/config.xml.user
+++ b/jenkins/files/config.xml.user
@@ -29,7 +29,7 @@
<insensitiveSearch>false</insensitiveSearch>
</hudson.search.UserSearchProperty>
<hudson.security.HudsonPrivateSecurityRealm_-Details>
- <passwordHash>#jbcrypt:{{ salt['jenkins_hash.encode_password'](user.password) }}</passwordHash>
+ <passwordHash>#jbcrypt:{{ salt['jenkins_common.encode_password'](user.password) }}</passwordHash>
</hudson.security.HudsonPrivateSecurityRealm_-Details>
{%- if user.public_keys is defined %}
<org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl>
diff --git a/jenkins/files/credentials.xml b/jenkins/files/credentials.xml
deleted file mode 100644
index fc6c244..0000000
--- a/jenkins/files/credentials.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-{%- from "jenkins/map.jinja" import master with context -%}
-<?xml version='1.0' encoding='UTF-8'?>
-<com.cloudbees.plugins.credentials.SystemCredentialsProvider plugin="credentials@2.1.4">
- <domainCredentialsMap class="hudson.util.CopyOnWriteMap$Hash">
- <entry>
- <com.cloudbees.plugins.credentials.domains.Domain>
- <specifications/>
- </com.cloudbees.plugins.credentials.domains.Domain>
- <java.util.concurrent.CopyOnWriteArrayList>
- {%- for credential in master.credentials %}
- {%- if credential.type == "username_password" %}
- <com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
- <scope>{{ credential.scope }}</scope>
- <id>{{ credential.id }}</id>
- <description>{{ credential.desc }}</description>
- <username>{{ credential.username }}</username>
- <password>{{ salt['jenkins_hash.encode_password'](credential.password) }}</password>
- </com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
- {%- elif credential.type == "ssh_key" %}
- <com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey plugin="ssh-credentials@1.12">
- <scope>{{ credential.scope }}</scope>
- <id>{{ credential.id }}</id>
- <description>{{ credential.desc }}</description>
- <username>{{ credential.username }}</username>
- {%- if credential.password is defined %}
- <passphrase>{{ salt['jenkins_hash.encode_password'](credential.password) }}</passphrase>
- {%- endif %}
- <privateKeySource class="com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$DirectEntryPrivateKeySource">
- <privateKey>{{ credential.key }}</privateKey>
- </privateKeySource>
- </com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey>
- {%- endif %}
- {%- endfor %}
- </java.util.concurrent.CopyOnWriteArrayList>
- </entry>
- </domainCredentialsMap>
-</com.cloudbees.plugins.credentials.SystemCredentialsProvider>
diff --git a/jenkins/master/service.sls b/jenkins/master/service.sls
index d0925cb..0946875 100644
--- a/jenkins/master/service.sls
+++ b/jenkins/master/service.sls
@@ -68,16 +68,6 @@
{%- if master.credentials is defined %}
-{{ master.home }}/credentials.xml:
- file.managed:
- - source: salt://jenkins/files/credentials.xml
- - template: jinja
- - user: jenkins
- - require:
- - pkg: jenkins_packages
-
-{%- endif %}
-
{%- if master.get('sudo', false) %}
/etc/sudoers.d/99-jenkins-user: