smart_proxy_realm_ad_plugin 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ # Status
2
+
3
+ Hi!!! Please try this out and test it. I need feedback.
4
+
5
+ # Description
6
+ This plugin adds a new realm provider for managing hosts in Active Directory.
7
+
8
+ Useful if you directly integrate with Active Directory and dont use FreeIPA.
9
+
10
+ If you use this plugin you let foreman-proxy provision/deprovision computer accounts and password and also distribute the password to a kickstart file.
11
+
12
+ It does the following:
13
+
14
+ * When hosts are created in foreman it will create a new computer account in active directory
15
+ and return the password to foreman so it can be used in the kickstart file.
16
+
17
+ * When hosts are rebuilt it will reset the computers account and return a new password to the kickstart file.
18
+
19
+ * When hosts are deleted in foreman it will delete the associated computer account in active directory.
20
+
21
+ ## Installation
22
+ See How_to_Install_a_Smart-Proxy_Plugin for how to install Smart Proxy plugins.
23
+
24
+ Install this gem:
25
+
26
+ First clone this repo:
27
+ ```
28
+ git clone https://github.com/martencassel/smart_proxy_realm_ad_plugin
29
+ ```
30
+
31
+ Then run bundle and gem install.
32
+
33
+ ```
34
+ cd smart_proxy_realm_ad_plugin
35
+ bundle install && gem build smart_proxy_realm_ad_plugin.gemspec \
36
+ && sudo gem install smart_proxy_realm_ad_plugin-0.1.gem
37
+
38
+ ```
39
+
40
+ Then add the depedencies to to smart-proxy bundler.d directory like below:
41
+
42
+ Edit 'bundler.d/Gemfile.local.rb' and set:
43
+
44
+ gem 'smart_proxy_realm_ad_plugin'
45
+ gem 'radcli'
46
+ gem 'rkerberos', '>= 0.1.1'
47
+ gem 'passgen'
48
+
49
+ ## Configuration
50
+
51
+ Then enable this as a realm provider in foreman-proxy
52
+
53
+ To enable this realm provider, edit `/etc/foreman-proxy/settings.d/realm.yml` and set:
54
+
55
+ :enabled: true
56
+
57
+ :use_provider: realm_ad
58
+
59
+ ## Testing
60
+
61
+ bundle exec rake test
62
+
63
+ ## Install dependencies
64
+
65
+ Install the gem dependencies first:
66
+
67
+ 1. rkerberos
68
+ 2. radcli
69
+
70
+ ### rkerberos
71
+ ```
72
+ sudo gem install rkerberos
73
+ ```
74
+
75
+ ### radcli
76
+
77
+ #### radcli prereqs (ubuntu)
78
+ ```
79
+ sudo apt-get install ruby gem ruby-dev
80
+ sudo gem install rake bundler rakecompiler rspec
81
+ sudo apt-get install automake autoconf xmlto xsltproc libkrb5-dev libldap2-dev libsasl2-dev
82
+ ```
83
+
84
+ ```
85
+ git clone https://github.com/martencassel/radcli
86
+ cd radcli
87
+ rake build
88
+ gem install pkg/radcli-0.1.0.gem
89
+ ```
90
+
91
+ ## Contributing
92
+
93
+ Fork and send a Pull Request. Thanks!
94
+
95
+ ## Copyright
96
+
97
+ Copyright (c) 2016,2017 Mårten Cassel
98
+
99
+ This program is free software: you can redistribute it and/or modify
100
+ it under the terms of the GNU General Public License as published by
101
+ the Free Software Foundation, either version 3 of the License, or
102
+ (at your option) any later version.
103
+
104
+ This program is distributed in the hope that it will be useful,
105
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
106
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
107
+ GNU General Public License for more details.
108
+
109
+ You should have received a copy of the GNU General Public License
110
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
111
+
@@ -0,0 +1,2 @@
1
+ gem 'smart_proxy_realm_ad_plugin'
2
+ gem 'radcli'
@@ -0,0 +1,24 @@
1
+ ---
2
+ # Authentication for Kerberos-based Realms
3
+ :realm: EXAMPLE.COM
4
+
5
+ # Kerberos pricipal used to authenticate against Active Directory
6
+ :principal: realm-proxy@EXAMPLE.COM
7
+
8
+ # Path to the keytab used to authenticate against Active Directory
9
+ :keytab_path: /etc/foreman-proxy/realm_ad.keytab
10
+
11
+ # FQDN of the Domain Controller
12
+ :domain_controller: dc.example.com
13
+
14
+ # Optional: OU where the machine account shall be placed
15
+ #:ou: OU=Linux,OU=Servers,DC=example,DC=com
16
+
17
+ # Optional: Prefix for the computername
18
+ #:computername_prefix: ''
19
+
20
+ # Optional: Generate the computername by calculating the SHA256 hexdigest of the hostname
21
+ #:computername_hash: false
22
+
23
+ # Optional: use the fqdn of the host to generate the computername
24
+ #:computername_use_fqdn: false
@@ -0,0 +1,23 @@
1
+ module Proxy::AdRealm
2
+ class ConfigurationLoader
3
+ def load_classes
4
+ require 'smart_proxy_realm_ad/provider'
5
+ end
6
+
7
+ def load_dependency_injection_wirings(container_instance, settings)
8
+ container_instance.dependency :realm_provider_impl,
9
+ lambda {
10
+ ::Proxy::AdRealm::Provider.new(
11
+ realm: settings[:realm],
12
+ keytab_path: settings[:keytab_path],
13
+ principal: settings[:principal],
14
+ domain_controller: settings[:domain_controller],
15
+ ou: settings[:ou],
16
+ computername_prefix: settings[:computername_prefix],
17
+ computername_hash: settings[:computername_hash],
18
+ computername_use_fqdn: settings[:computername_use_fqdn]
19
+ )
20
+ }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ require 'smart_proxy_realm_ad/version'
2
+
3
+ module Proxy::AdRealm
4
+ class Plugin < Proxy::Provider
5
+ load_classes ::Proxy::AdRealm::ConfigurationLoader
6
+ load_dependency_injection_wirings ::Proxy::AdRealm::ConfigurationLoader
7
+
8
+ validate_presence :realm, :keytab_path, :principal, :domain_controller
9
+
10
+ plugin :realm_ad, ::Proxy::AdRealm::VERSION
11
+ end
12
+ end
@@ -0,0 +1,131 @@
1
+ require 'proxy/kerberos'
2
+ require 'radcli'
3
+ require 'digest'
4
+
5
+ module Proxy::AdRealm
6
+ class Provider
7
+ include Proxy::Log
8
+ include Proxy::Util
9
+ include Proxy::Kerberos
10
+
11
+ attr_reader :realm, :keytab_path, :principal, :domain_controller, :domain, :ou, :computername_prefix, :computername_hash, :computername_use_fqdn
12
+
13
+ def initialize(options = {})
14
+ @realm = options[:realm]
15
+ @keytab_path = options[:keytab_path]
16
+ @principal = options[:principal]
17
+ @domain_controller = options[:domain_controller]
18
+ @domain = options[:realm].downcase
19
+ @ou = options[:ou]
20
+ @computername_prefix = options[:computername_prefix]
21
+ @computername_hash = options.fetch(:computername_hash, false)
22
+ @computername_use_fqdn = options.fetch(:computername_use_fqdn, false)
23
+ logger.info 'Proxy::AdRealm: initialize...'
24
+ end
25
+
26
+ def check_realm(realm)
27
+ raise Exception, "Unknown realm #{realm}" unless realm.casecmp(@realm).zero?
28
+ end
29
+
30
+ def find(_hostfqdn)
31
+ true
32
+ end
33
+
34
+ def create(realm, hostfqdn, params)
35
+ logger.info "Proxy::AdRealm: create... #{realm}, #{hostfqdn}, #{params}"
36
+ check_realm(realm)
37
+ kinit_radcli_connect
38
+
39
+ password = generate_password
40
+ result = { randompassword: password }
41
+
42
+ computername = hostfqdn_to_computername(hostfqdn)
43
+
44
+ if params[:rebuild] == 'true'
45
+ radcli_password(computername, password)
46
+ else
47
+ radcli_join(hostfqdn, computername, password)
48
+ end
49
+
50
+ JSON.pretty_generate(result)
51
+ end
52
+
53
+ def delete(realm, hostfqdn)
54
+ logger.info "Proxy::AdRealm: delete... #{realm}, #{hostfqdn}"
55
+ kinit_radcli_connect
56
+ check_realm(realm)
57
+ computername = hostfqdn_to_computername(hostfqdn)
58
+ radcli_delete(computername)
59
+ end
60
+
61
+ private
62
+
63
+ def hostfqdn_to_computername(hostfqdn)
64
+ computername = hostfqdn
65
+
66
+ # strip the domain from the host
67
+ computername = computername.split('.').first unless computername_use_fqdn
68
+
69
+ # generate the SHA256 hexdigest from the computername
70
+ computername = Digest::SHA256.hexdigest(computername) if computername_hash
71
+
72
+ # apply prefix if it has not already been applied
73
+ computername = computername_prefix + computername if apply_computername_prefix?(computername)
74
+
75
+ # limit length to 15 characters and upcase the computername
76
+ # see https://support.microsoft.com/en-us/kb/909264
77
+ computername[0, 15].upcase
78
+ end
79
+
80
+ def apply_computername_prefix?(computername)
81
+ !computername_prefix.nil? && !computername_prefix.empty? && (computername_hash || !computername[0, computername_prefix.size].casecmp(computername_prefix).zero?)
82
+ end
83
+
84
+ def kinit_radcli_connect
85
+ init_krb5_ccache(@keytab_path, @principal)
86
+ @adconn = radcli_connect
87
+ end
88
+
89
+ def radcli_connect
90
+ # Connect to active directory
91
+ conn = Adcli::AdConn.new(@domain)
92
+ conn.set_domain_realm(@realm)
93
+ conn.set_domain_controller(@domain_controller)
94
+ conn.set_login_ccache_name('')
95
+ conn.connect
96
+ conn
97
+ end
98
+
99
+ def radcli_join(hostfqdn, computername, password)
100
+ # Join computer
101
+ enroll = Adcli::AdEnroll.new(@adconn)
102
+ enroll.set_computer_name(computername)
103
+ enroll.set_host_fqdn(hostfqdn)
104
+ enroll.set_domain_ou(@ou) if @ou
105
+ enroll.set_computer_password(password)
106
+ enroll.join
107
+ end
108
+
109
+ def generate_password
110
+ characters = ('A'..'Z').to_a + ('a'..'z').to_a + (0..9).to_a
111
+ Array.new(20) { characters.sample }.join
112
+ end
113
+
114
+ def radcli_password(computername, password)
115
+ # Reset a computer's password
116
+ enroll = Adcli::AdEnroll.new(@adconn)
117
+ enroll.set_computer_name(computername)
118
+ enroll.set_domain_ou(@ou) if @ou
119
+ enroll.set_computer_password(password)
120
+ enroll.password
121
+ end
122
+
123
+ def radcli_delete(computername)
124
+ # Delete a computer's account
125
+ enroll = Adcli::AdEnroll.new(@adconn)
126
+ enroll.set_computer_name(computername)
127
+ enroll.set_domain_ou(@ou) if @ou
128
+ enroll.delete
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,5 @@
1
+ module Proxy
2
+ module AdRealm
3
+ VERSION = '0.1'.freeze
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ require 'smart_proxy_realm_ad/configuration_loader'
2
+ require 'smart_proxy_realm_ad/plugin'
3
+
4
+ module Proxy::AdRealm; end
@@ -0,0 +1,125 @@
1
+ require 'rubygems'
2
+ require 'test_helper'
3
+ require 'smart_proxy_realm_ad/provider'
4
+ require 'radcli'
5
+
6
+ class RealmAdTest < Test::Unit::TestCase
7
+ def setup
8
+ @realm = 'test_realm'
9
+ @provider = Proxy::AdRealm::Provider.new(
10
+ realm: 'example.com',
11
+ keytab_path: 'keytab_path',
12
+ principal: 'principal',
13
+ domain_controller: 'domain-controller',
14
+ ou: nil,
15
+ computername_prefix: nil,
16
+ computername_hash: false,
17
+ computername_use_fqdn: false
18
+ )
19
+ end
20
+
21
+ def test_create_host
22
+ hostname = 'host.example.com'
23
+ computername = 'HOST'
24
+ params = {
25
+ rebuild: 'false'
26
+ }
27
+ @provider.expects(:check_realm).with(@realm)
28
+ @provider.expects(:kinit_radcli_connect)
29
+ @provider.expects(:generate_password).returns('password')
30
+ @provider.expects(:radcli_join).with(hostname, computername, 'password')
31
+ @provider.create(@realm, hostname, params)
32
+ end
33
+
34
+ def test_create_host_generates_password
35
+ hostname = 'host.example.com'
36
+ params = {
37
+ rebuild: 'false'
38
+ }
39
+ @provider.expects(:check_realm).with(@realm)
40
+ @provider.expects(:kinit_radcli_connect)
41
+ @provider.expects(:radcli_join)
42
+ response = JSON.parse(@provider.create(@realm, hostname, params))
43
+ assert_kind_of String, response['randompassword']
44
+ assert_equal 20, response['randompassword'].size
45
+ end
46
+
47
+ def test_create_host_prefixes_computername
48
+ hostname = 'host.example.com'
49
+ computername = 'ORG-HOST'
50
+ params = {
51
+ rebuild: 'false'
52
+ }
53
+ @provider.stubs(:computername_prefix).returns('ORG-')
54
+ @provider.expects(:check_realm).with(@realm)
55
+ @provider.expects(:kinit_radcli_connect)
56
+ @provider.expects(:generate_password).returns('password')
57
+ @provider.expects(:radcli_join).with(hostname, computername, 'password')
58
+ @provider.create(@realm, hostname, params)
59
+ end
60
+
61
+ def test_create_host_limits_computername_to_15_characters
62
+ hostname = 'superlonghostname.example.com'
63
+ computername = 'SUPERLONGHOSTNA'
64
+ params = {
65
+ rebuild: 'false'
66
+ }
67
+ @provider.expects(:check_realm).with(@realm)
68
+ @provider.expects(:kinit_radcli_connect)
69
+ @provider.expects(:generate_password).returns('password')
70
+ @provider.expects(:radcli_join).with(hostname, computername, 'password')
71
+ @provider.create(@realm, hostname, params)
72
+ end
73
+
74
+ def test_create_host_applies_sha256_to_computername
75
+ hostname = 'host.example.com'
76
+ computername = '4740AE6347B0172'
77
+ params = {
78
+ rebuild: 'false'
79
+ }
80
+ @provider.stubs(:computername_hash).returns(true)
81
+ @provider.expects(:check_realm).with(@realm)
82
+ @provider.expects(:kinit_radcli_connect)
83
+ @provider.expects(:generate_password).returns('password')
84
+ @provider.expects(:radcli_join).with(hostname, computername, 'password')
85
+ @provider.create(@realm, hostname, params)
86
+ end
87
+
88
+ def test_create_with_unrecognized_realm_raises_exception
89
+ assert_raises(Exception) { @provider.create('unknown_realm', 'a_host', {}) }
90
+ end
91
+
92
+ def test_create_rebuild
93
+ hostname = 'host.example.com'
94
+ params = {}
95
+ params[:rebuild] = 'true'
96
+ @provider.expects(:check_realm).with(@realm)
97
+ @provider.expects(:kinit_radcli_connect)
98
+ @provider.expects(:radcli_password)
99
+ response = JSON.parse(@provider.create(@realm, hostname, params))
100
+ assert_kind_of String, response['randompassword']
101
+ assert_equal 20, response['randompassword'].size
102
+ end
103
+
104
+ def test_rebuild_with_unrecognized_realm_raises_exception
105
+ params = {}
106
+ params[:rebuild] = 'true'
107
+ assert_raises(Exception) { @provider.create('unknown_realm', 'a_host', params) }
108
+ end
109
+
110
+ def test_find
111
+ assert_true @provider.find('a_host_fqdn')
112
+ end
113
+
114
+ def test_delete
115
+ @provider.expects(:check_realm).with(@realm)
116
+ @provider.expects(:kinit_radcli_connect)
117
+ @provider.expects(:radcli_delete)
118
+ @provider.delete(@realm, 'a_host')
119
+ end
120
+
121
+ def test_delete_unrecognized_realm_raises_exception
122
+ @provider.expects(:kinit_radcli_connect)
123
+ assert_raises(Exception) { @provider.delete('unkown_realm', 'a_host') }
124
+ end
125
+ end