smart_proxy_realm_ad_plugin 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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