smart_proxy_realm_ad_plugin 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +675 -0
- data/README.md +111 -0
- data/bundler.d/realm_ad_plugin.rb +2 -0
- data/config/realm_ad.yml.example +24 -0
- data/lib/smart_proxy_realm_ad/configuration_loader.rb +23 -0
- data/lib/smart_proxy_realm_ad/plugin.rb +12 -0
- data/lib/smart_proxy_realm_ad/provider.rb +131 -0
- data/lib/smart_proxy_realm_ad/version.rb +5 -0
- data/lib/smart_proxy_realm_ad_plugin.rb +4 -0
- data/test/ad_provider_test.rb +125 -0
- data/test/api_tests.sh +14 -0
- data/test/setup_ad.md +36 -0
- data/test/test_helper.rb +7 -0
- data/test/test_radcli.rb +57 -0
- metadata +133 -0
data/README.md
ADDED
@@ -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,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,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
|