Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
elconas authored and madAndroid committed May 22, 2017
1 parent 8607cbd commit 664d35f
Show file tree
Hide file tree
Showing 17 changed files with 333 additions and 176 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,46 @@ the module.**

See [NATIVE_TYPES_AND_PROVIDERS.md](NATIVE_TYPES_AND_PROVIDERS.md)

# Jenkins 2.54 and 2.46.2 remoting free CLI and username / password CLI auth

## Remoting Free CLI

Jenkins refactored the CLI in 2.54 and 2.46.2 in response to several security
incidents (See [JENKINS-41745](https://issues.jenkins-ci.org/browse/JENKINS-41745). This module has been adjusted to support the new CLI. However you
need to tell the module if the new remoting free cli is in place. Please set
```$::jenkins::cli_remoting_free``` to true for latest Jenkins servers.

Note: This is not the default to support backward compatibility. This may become
a default once this module is released in a new version.

Also note that the module tries to do heuristics for this setting if not specified.
You can always set this parameter to enforce usage of old or new CLI interface.

Heuristics:

* LTS:
* If manage_repo and repo == lts and version = latest -> true
* If manage_repo and repo == lts and version >= 2.46.2 -> true
* else false
* Non-LTS:
* If manage_repo and repo != lts and version = latest -> true
* If manage_repo and repo != lts and version >= 2.54 -> true
* else false

## Username and Password Auth

The new CLI also supports proper authentication with username and password. This
has not been supported in this module for a long time, but is a requirement for
supporting AD and OpenID authentications (there is no ssh key there). You can now
also supply ```$::jenkins::cli_username``` and ```$::jenkins::cli_password``` to
use username / password based authentication. Then the puppet automation user can
also reside in A.D

Note: latest jenkins (2.54++ and 2.46.2++) require a ssh username, so you must also
provide ```$::jenkins::cli_username``` for ssh. If you specify both username/password
and ssh key file, SSH authentication is preferred.


# Using puppet-jenkins

## Getting Started
Expand Down
18 changes: 8 additions & 10 deletions files/puppet_helper.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,11 @@ class Actions {
/////////////////////////
// create or update user from JSON
/////////////////////////
void user_update() { // or create
void user_update(String jsonfile) { // or create
// parse JSON doc from stdin
def slurper = new groovy.json.JsonSlurper()
def text = bindings.stdin.text
def conf = slurper.parseText(text)
def conf = slurper.parse(new File(jsonfile))


// a user id is required
def id = conf['id']
Expand Down Expand Up @@ -507,7 +507,7 @@ class Actions {
info['description'] = cred.description
info['username'] = cred.username
info['private_key'] = cred.privateKey
info['passphrase'] = cred.passphrase?.plainText
if (cred.passphrase) { info['passphrase'] = cred.passphrase.plainText } else { info['passphrase'] = '' }
break
case 'com.dabsquared.gitlabjenkins.connection.GitLabApiTokenImpl':
info['apiToken'] = cred.apiToken.plainText
Expand Down Expand Up @@ -559,13 +559,12 @@ class Actions {
* modify an existing credentials specified by a JSON document passed via
* the stdin
*/
void credentials_update_json() {
void credentials_update_json(String jsonfile) {
def j = Jenkins.getInstance()

// parse JSON doc from stdin
def slurper = new groovy.json.JsonSlurper()
def text = bindings.stdin.text
def conf = slurper.parseText(text)
def conf = slurper.parse(new File(jsonfile))

def cred = null
switch (conf['impl']) {
Expand Down Expand Up @@ -844,7 +843,7 @@ class Actions {
////////////////////////
// set_jenkins_instance
////////////////////////
void set_jenkins_instance() {
void set_jenkins_instance(String jsonfile) {
def j = Jenkins.getInstance()

def setup = { info ->
Expand All @@ -864,8 +863,7 @@ class Actions {

// parse JSON doc from stdin
def slurper = new groovy.json.JsonSlurper()
def text = bindings.stdin.text
def conf = slurper.parseText(text)
def conf = slurper.parse(new File(jsonfile))

// each key in the hash is a method on the Jenkins singleton. The key's
// value is an object to instantiate and pass to the method. (currently,
Expand Down
16 changes: 10 additions & 6 deletions lib/puppet_x/jenkins/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ class PuppetX::Jenkins::Config
class UnknownConfig < ArgumentError; end

DEFAULTS = {
:cli_jar => '/usr/lib/jenkins/jenkins-cli.jar',
:url => 'http://localhost:8080',
:ssh_private_key => nil,
:puppet_helper => '/usr/lib/jenkins/puppet_helper.groovy',
:cli_tries => 30,
:cli_try_sleep => 2,
:cli_jar => '/usr/lib/jenkins/jenkins-cli.jar',
:url => 'http://localhost:8080',
:ssh_private_key => nil,
:puppet_helper => '/usr/lib/jenkins/puppet_helper.groovy',
:cli_tries => 30,
:cli_try_sleep => 2,
:cli_username => nil,
:cli_password => nil,
:cli_password_file => '/tmp/jenkins_credentials_for_puppet',
:cli_remoting_free => false,
}
CONFIG_CLASS = 'jenkins::cli::config'
FACT_PREFIX = 'jenkins_'
Expand Down
83 changes: 63 additions & 20 deletions lib/puppet_x/jenkins/provider/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,21 +96,32 @@ def cli(command, options = nil)
self.class.cli(*args)
end

def self.clihelper(command, options = nil)
catalog = options.nil? ? nil : options[:catalog]
def self.clihelper(command, options = {})
catalog = options.key?(:catalog) ? options[:catalog] : nil
config = PuppetX::Jenkins::Config.new(catalog)

puppet_helper = config[:puppet_helper]
cli_remoting_free = config[:cli_remoting_free]

if cli_remoting_free
cli_pre_cmd = ['/bin/cat', puppet_helper, '|']
cli_cmd = ['groovy', '=' ] + [command]
options[:tmpfile_as_param]=true
else
cli_pre_cmd = []
cli_cmd = ['groovy', puppet_helper] + [command]
end

cli_cmd = ['groovy', puppet_helper] + [command]
cli_pre_cmd.flatten!
cli_cmd.flatten!

cli(cli_cmd, options)
cli(cli_cmd, options, cli_pre_cmd)
end

def self.cli(command, options = {})
def self.cli(command, options = {}, cli_pre_cmd = [])

if options.nil? || !options.key?(:stdinjson) && !options.key?(:stdin)
return execute_with_retry(command, options)
return execute_with_retry(command, options, cli_pre_cmd)
end

if options.key?(:stdinjson)
Expand All @@ -122,22 +133,31 @@ def self.cli(command, options = {})
input = options.delete(:stdin)
end

if options.key?(:tmpfile_as_param)
tmpfile_as_param = options[:tmpfile_as_param]
else
tmpfile_as_param = false
end

Puppet.debug("#{sname} stdin:\n#{input}")

# a tempfile block arg is not used to simplify mock testing :/
tmp = Tempfile.open(sname)
tmp.write input
tmp.flush
options[:stdinfile] = tmp.path
result = execute_with_retry(command, options)
FileUtils.chown 'jenkins', 'jenkins', tmp.path if tmpfile_as_param and File.exists?(tmp.path)
result = execute_with_retry(command, options, cli_pre_cmd)
tmp.close
tmp.unlink

result
end

def self.execute_with_retry(command, options = {})
def self.execute_with_retry(command, options = {}, cli_pre_cmd = [])
options ||= {}
cli_pre_cmd ||= []

catalog = options.delete(:catalog)

options.merge!({ :failonfail => true })
Expand All @@ -146,13 +166,17 @@ def self.execute_with_retry(command, options = {})
options.merge!({ :combine => true })

config = PuppetX::Jenkins::Config.new(catalog)
cli_jar = config[:cli_jar]
url = config[:url]
ssh_private_key = config[:ssh_private_key]
cli_tries = config[:cli_tries]
cli_try_sleep = config[:cli_try_sleep]

base_cmd = [
cli_jar = config[:cli_jar]
url = config[:url]
ssh_private_key = config[:ssh_private_key]
cli_tries = config[:cli_tries]
cli_try_sleep = config[:cli_try_sleep]
cli_username = config[:cli_username]
cli_password = config[:cli_password]
cli_password_file = config[:cli_password_file]
cli_remoting_free = config[:cli_remoting_free]

base_cmd = cli_pre_cmd + [
command(:java),
'-jar', cli_jar,
'-s', url,
Expand All @@ -162,10 +186,20 @@ def self.execute_with_retry(command, options = {})
cli_cmd.flatten!

auth_cmd = nil
unless ssh_private_key.nil?
auth_cmd = base_cmd + ['-i', ssh_private_key] + [command]
auth_cmd.flatten!
if !ssh_private_key.nil?
if cli_remoting_free
auth_cmd = base_cmd + ['-i', ssh_private_key] + ['-ssh', '-user', cli_username] + [command]
else
auth_cmd = base_cmd + ['-i', ssh_private_key] + [command]
end
elsif !cli_username.nil? and !cli_password.nil?
if cli_remoting_free
auth_cmd = base_cmd + ['-auth', "@#{cli_password_file}"] + [command]
else
auth_cmd = base_cmd + ['-username', cli_username, '-password', cli_password] + [command]
end
end
auth_cmd.flatten! unless auth_cmd.nil?

# retry on "unknown" execution errors but don't catch AuthErrors. If an
# AuthError has bubbled up to this level it means either an ssh_private_key
Expand Down Expand Up @@ -221,7 +255,7 @@ def self.execute_with_auth(cli_cmd, auth_cmd, options = {})

# convert Puppet::ExecutionFailure into a ::AuthError exception if it appears
# that the command failure was due to an authication problem
def self.execute_exceptionify(*args)
def self.execute_exceptionify(cmd, options)
cli_auth_errors = [
'You must authenticate to access this Jenkins.',
'anonymous is missing the Overall/Read permission',
Expand All @@ -235,9 +269,18 @@ def self.execute_exceptionify(*args)
'java.net.ConnectException: Connection refused',
'java.io.IOException: Failed to connect',
]

if options.key?(:tmpfile_as_param)
tmpfile_as_param = options[:tmpfile_as_param]
end

begin
#return Puppet::Provider.execute(*args)
return superclass.execute(*args)
if tmpfile_as_param and options.key?(:stdinfile)
return superclass.execute([ cmd, options[:stdinfile] ].flatten().join(' '), options)
else
return superclass.execute([ cmd ].flatten().join(' '), options)
end
rescue Puppet::ExecutionFailure => e
cli_auth_errors.each do |error|
if e.message.match(error)
Expand Down
27 changes: 14 additions & 13 deletions manifests/cli.pp
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,21 @@
$move_jar = "mv WEB-INF/jenkins-cli.jar ${jar}"
$remove_dir = 'rm -rf WEB-INF'

exec { 'jenkins-cli' :
command => "${extract_jar} && ${move_jar} && ${remove_dir}",
path => ['/bin', '/usr/bin'],
cwd => '/tmp',
# make sure we always call Exec[jenlins-cli] in case
# the binary does not exist
exec { 'check-jenkins-cli':
command => '/bin/true',
creates => $jar,
require => Service['jenkins'],
}
~> exec { 'jenkins-cli' :
command => "${extract_jar} && ${move_jar} && ${remove_dir}",
path => ['/bin', '/usr/bin'],
cwd => '/tmp',
refreshonly => true,
require => Service['jenkins'],
}
# Extract latest CLI in case package is updated / downgraded
Package[$::jenkins::package_name] ~> Exec['jenkins-cli']

file { $jar:
ensure => file,
Expand All @@ -46,20 +54,13 @@
$port = jenkins_port()
$prefix = jenkins_prefix()

# Provide the -i flag if specified by the user.
if $::jenkins::cli_ssh_keyfile {
$auth_arg = "-i ${::jenkins::cli_ssh_keyfile}"
} else {
$auth_arg = undef
}

# The jenkins cli command with required parameter(s)
$cmd = join(
delete_undef_values([
'java',
"-jar ${::jenkins::cli::jar}",
"-s http://localhost:${port}${prefix}",
$auth_arg,
$::jenkins::_cli_auth_arg,
]),
' '
)
Expand Down
29 changes: 29 additions & 0 deletions manifests/cli/config.pp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
$puppet_helper = undef,
$cli_tries = undef,
$cli_try_sleep = undef,
$cli_username = undef,
$cli_password = undef,
$cli_password_file = '/tmp/jenkins_credentials_for_puppet',
$cli_remoting_free = undef,
$ssh_private_key_content = undef,
) {
if $cli_jar { validate_absolute_path($cli_jar) }
Expand All @@ -21,6 +25,10 @@
if $puppet_helper { validate_absolute_path($puppet_helper) }
if $cli_tries { validate_integer($cli_tries) }
if $cli_try_sleep { validate_numeric($cli_try_sleep) }
if $cli_username { validate_string($cli_username) }
if $cli_password { validate_string($cli_password) }
if $cli_password_file { validate_absolute_path($cli_password_file) }
if $cli_remoting_free != undef { validate_bool($cli_remoting_free) }
validate_string($ssh_private_key_content)

if str2bool($::is_pe) {
Expand Down Expand Up @@ -61,4 +69,25 @@
}
}
}

if $cli_username and $cli_password {
file { $cli_password_file:
ensure => 'file',
mode => '0400',
backup => false,
content => "${cli_username}:${cli_password}",
}

# allow this class to be included when not running as root
if $::id == 'root' {
File[$cli_password_file] {
# the owner/group should probably be set externally and retrieved if
# present in the manfiest. At present, there is no authoritative place
# to retrive this information from.
owner => 'jenkins',
group => 'jenkins',
}
}
}

}
2 changes: 1 addition & 1 deletion manifests/cli/exec.pp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
)

if $unless {
$environment_run = [ "HELPER_CMD=${::jenkins::cli_helper::helper_cmd}" ]
$environment_run = [ "HELPER_CMD=eval ${::jenkins::cli_helper::helper_cmd}" ]
} else {
$environment_run = undef
}
Expand Down
Loading

0 comments on commit 664d35f

Please # to comment.