scooter 0.0.0 → 3.2.19
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.
- checksums.yaml +15 -0
- data/.env +5 -0
- data/.gitignore +47 -19
- data/Gemfile +3 -0
- data/HISTORY.md +1539 -0
- data/README.md +69 -10
- data/Rakefile +7 -0
- data/docs/http_dispatchers.md +79 -0
- data/lib/scooter.rb +11 -3
- data/lib/scooter/httpdispatchers.rb +12 -0
- data/lib/scooter/httpdispatchers/activity.rb +46 -0
- data/lib/scooter/httpdispatchers/activity/v1/v1.rb +50 -0
- data/lib/scooter/httpdispatchers/classifier.rb +376 -0
- data/lib/scooter/httpdispatchers/classifier/v1/v1.rb +99 -0
- data/lib/scooter/httpdispatchers/code_manager.rb +31 -0
- data/lib/scooter/httpdispatchers/code_manager/v1/v1.rb +17 -0
- data/lib/scooter/httpdispatchers/consoledispatcher.rb +132 -0
- data/lib/scooter/httpdispatchers/httpdispatcher.rb +168 -0
- data/lib/scooter/httpdispatchers/orchestrator/v1/v1.rb +87 -0
- data/lib/scooter/httpdispatchers/orchestratordispatcher.rb +83 -0
- data/lib/scooter/httpdispatchers/puppetdb/v4/v4.rb +51 -0
- data/lib/scooter/httpdispatchers/puppetdbdispatcher.rb +390 -0
- data/lib/scooter/httpdispatchers/rbac.rb +231 -0
- data/lib/scooter/httpdispatchers/rbac/v1/directory_service.rb +68 -0
- data/lib/scooter/httpdispatchers/rbac/v1/v1.rb +116 -0
- data/lib/scooter/ldap.rb +349 -0
- data/lib/scooter/ldap/ldap_fixtures.rb +60 -0
- data/lib/scooter/middleware/rbac_auth_token.rb +35 -0
- data/lib/scooter/utilities.rb +9 -0
- data/lib/scooter/utilities/beaker_utilities.rb +41 -0
- data/lib/scooter/utilities/string_utilities.rb +32 -0
- data/lib/scooter/version.rb +3 -1
- data/scooter.gemspec +23 -6
- data/spec/scooter/beaker_utilities_spec.rb +53 -0
- data/spec/scooter/httpdispatchers/activity/activity_spec.rb +218 -0
- data/spec/scooter/httpdispatchers/classifier/classifier_spec.rb +542 -0
- data/spec/scooter/httpdispatchers/code_manager/code-manager_spec.rb +67 -0
- data/spec/scooter/httpdispatchers/consoledispatcher_spec.rb +80 -0
- data/spec/scooter/httpdispatchers/httpdispatcher_spec.rb +91 -0
- data/spec/scooter/httpdispatchers/middleware/rbac_auth_token_spec.rb +58 -0
- data/spec/scooter/httpdispatchers/orchestratordispatcher_spec.rb +195 -0
- data/spec/scooter/httpdispatchers/puppetdbdispatcher_spec.rb +246 -0
- data/spec/scooter/httpdispatchers/rbac/rbac_spec.rb +387 -0
- data/spec/scooter/string_utilities_spec.rb +83 -0
- data/spec/spec_helper.rb +8 -0
- metadata +270 -18
- data/LICENSE.txt +0 -15
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
%w( v1 ).each do |lib|
|
|
2
|
+
require "scooter/httpdispatchers/rbac/v1/#{lib}"
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
module Scooter
|
|
6
|
+
module HttpDispatchers
|
|
7
|
+
# Methods added here are not representative of endpoints, but are more
|
|
8
|
+
# generalized to be helper methods to to acquire data, such as getting
|
|
9
|
+
# the id of a user based on their login name. Be cautious about using
|
|
10
|
+
# these methods if you are utilizing a dispatcher with credentials;
|
|
11
|
+
# the user is not guaranteed to have privileges for all the methods
|
|
12
|
+
# defined here, or the user may not be signed in. If you have a method
|
|
13
|
+
# defined here that is using the connection object directly, you should
|
|
14
|
+
# probably be using a method defined in the version module instead.
|
|
15
|
+
module Rbac
|
|
16
|
+
|
|
17
|
+
include Scooter::HttpDispatchers::Rbac::V1
|
|
18
|
+
include Scooter::Utilities
|
|
19
|
+
|
|
20
|
+
def set_rbac_path(connection=self.connection)
|
|
21
|
+
set_url_prefix
|
|
22
|
+
connection.url_prefix.path = '/rbac-api'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def generate_local_user(options = {})
|
|
26
|
+
email = options['email'] || "#{RandomString.generate(8)}@example.com"
|
|
27
|
+
display_name = options['display_name'] || RandomString.generate(8)
|
|
28
|
+
login = options['login'] || RandomString.generate(16)
|
|
29
|
+
role_ids = options['role_ids'] || []
|
|
30
|
+
password = options['password'] || 'Puppet11'
|
|
31
|
+
|
|
32
|
+
user_hash = { 'email' => email,
|
|
33
|
+
'display_name' => display_name,
|
|
34
|
+
'login' => login,
|
|
35
|
+
'role_ids' => role_ids,
|
|
36
|
+
'password' => password }
|
|
37
|
+
|
|
38
|
+
response = create_local_user(user_hash)
|
|
39
|
+
return response if response.env.status != 200
|
|
40
|
+
Scooter::HttpDispatchers::ConsoleDispatcher.new(@host,
|
|
41
|
+
login: login,
|
|
42
|
+
password: password)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def generate_role(options = {})
|
|
46
|
+
permissions = options['permissions'] || []
|
|
47
|
+
user_ids = options['user_ids'] || []
|
|
48
|
+
group_ids = options['group_ids'] || []
|
|
49
|
+
display_name = options['display_name'] || RandomString.generate
|
|
50
|
+
description = options['description'] || RandomString.generate
|
|
51
|
+
|
|
52
|
+
role_hash = { 'permissions' => permissions,
|
|
53
|
+
'user_ids' => user_ids,
|
|
54
|
+
'group_ids' => group_ids,
|
|
55
|
+
'display_name' => display_name,
|
|
56
|
+
'description' => description }
|
|
57
|
+
|
|
58
|
+
response = create_role(role_hash)
|
|
59
|
+
return response if response.env.status != 200
|
|
60
|
+
response.env.body
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def delete_role_by_name(role_name)
|
|
64
|
+
role_id = get_role_id(role_name)
|
|
65
|
+
delete_role(role_id)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def add_user_to_role(console_dispatcher, role)
|
|
69
|
+
user_id = get_user_id_of_console_dispatcher(console_dispatcher)
|
|
70
|
+
role['user_ids'].push(user_id)
|
|
71
|
+
replace_role(role)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def remove_user_from_role(console_dispatcher, role)
|
|
75
|
+
user_id = get_user_id_of_console_dispatcher(console_dispatcher)
|
|
76
|
+
role['user_ids'].delete(user_id)
|
|
77
|
+
replace_role(role)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def get_user_id_of_console_dispatcher(console_dispatcher)
|
|
81
|
+
return get_user_id_by_login_name('api_user') if console_dispatcher.credentials == nil
|
|
82
|
+
get_user_id_by_login_name(console_dispatcher.credentials.login)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def get_current_user_id
|
|
86
|
+
get_current_user_data['id']
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def get_console_dispatcher_data(console_dispatcher)
|
|
90
|
+
users = get_list_of_users
|
|
91
|
+
users.each do |user|
|
|
92
|
+
return user if user['login'] == console_dispatcher.credentials.login
|
|
93
|
+
end
|
|
94
|
+
nil #return nil if the console dispatcher is not found
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def update_console_dispatcher(update_hash, console_dispatcher)
|
|
98
|
+
user = get_console_dispatcher_data(console_dispatcher)
|
|
99
|
+
user.merge!(update_hash)
|
|
100
|
+
update_local_user(user)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def revoke_console_dispatcher(console_dispatcher)
|
|
104
|
+
update_console_dispatcher({ 'is_revoked' => true }, console_dispatcher)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def get_user_id_by_login_name(name)
|
|
108
|
+
users = get_list_of_users
|
|
109
|
+
users.each do |user|
|
|
110
|
+
return user['id'] if user['login'] == name
|
|
111
|
+
end
|
|
112
|
+
nil #return nil if name is not found
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def delete_local_console_dispatcher(console_dispatcher)
|
|
116
|
+
uuid = get_user_id_of_console_dispatcher(console_dispatcher)
|
|
117
|
+
delete_local_user(uuid)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def get_group_data_by_name(name)
|
|
121
|
+
groups = get_list_of_groups
|
|
122
|
+
groups.each do |group|
|
|
123
|
+
return group if name == group['login']
|
|
124
|
+
end
|
|
125
|
+
nil #return nil if name is not found
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def get_group_id(group_name)
|
|
129
|
+
groups = get_list_of_groups
|
|
130
|
+
groups.each do |group|
|
|
131
|
+
return group['id'] if group_name == group['display_name']
|
|
132
|
+
end
|
|
133
|
+
nil #return nil if group_name not found
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def get_role_by_name(role_name)
|
|
137
|
+
roles = get_list_of_roles
|
|
138
|
+
roles.each do |role|
|
|
139
|
+
return role if role['display_name'] == role_name
|
|
140
|
+
end
|
|
141
|
+
nil # return nil if role_name not found
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def get_role_id(role_name)
|
|
145
|
+
roles = get_list_of_roles
|
|
146
|
+
roles.each do |role|
|
|
147
|
+
return role['id'] if role['display_name'] == role_name
|
|
148
|
+
end
|
|
149
|
+
nil #return nil if role_name not found
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def reset_console_dispatcher_password(console_dispatcher, password)
|
|
153
|
+
token = get_password_reset_token_for_console_dispatcher(console_dispatcher)
|
|
154
|
+
reset_local_user_password(token, password)
|
|
155
|
+
console_dispatcher.credentials.password = password
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def reset_console_dispatcher_password_to_default(console_dispatcher)
|
|
159
|
+
token = get_password_reset_token_for_console_dispatcher(console_dispatcher)
|
|
160
|
+
reset_local_user_password(token, 'Puppet11')
|
|
161
|
+
console_dispatcher.credentials.password = 'Puppet11'
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def get_password_reset_token_for_console_dispatcher(console_dispatcher)
|
|
165
|
+
uuid = get_user_id_of_console_dispatcher(console_dispatcher)
|
|
166
|
+
create_password_reset_token(uuid)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def acquire_token_with_credentials(lifetime=nil)
|
|
170
|
+
@token = acquire_token(credentials.login, credentials.password, lifetime)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def rbac_database_matches_self?(host_name)
|
|
174
|
+
original_host_name = self.host
|
|
175
|
+
begin
|
|
176
|
+
self.host = host_name.to_s
|
|
177
|
+
initialize_connection
|
|
178
|
+
other_users = get_list_of_users
|
|
179
|
+
other_groups = get_list_of_groups
|
|
180
|
+
other_roles = get_list_of_roles
|
|
181
|
+
ensure
|
|
182
|
+
self.host = original_host_name
|
|
183
|
+
initialize_connection
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
self_users = get_list_of_users
|
|
187
|
+
self_groups = get_list_of_groups
|
|
188
|
+
self_roles = get_list_of_roles
|
|
189
|
+
|
|
190
|
+
errors = ''
|
|
191
|
+
errors << "Users do not match\r\n" unless users_match?(self_users, other_users)
|
|
192
|
+
errors << "Groups do not match\r\n" unless groups_match?(self_groups, other_groups)
|
|
193
|
+
errors << "Roles do not match\r\n" unless roles_match?(self_roles, other_roles)
|
|
194
|
+
|
|
195
|
+
@faraday_logger.warn(errors.chomp) unless errors.empty?
|
|
196
|
+
errors.empty?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
def users_match?(other_users, self_users)
|
|
202
|
+
other_users == self_users
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def groups_match?(other_groups, self_groups)
|
|
206
|
+
other_groups == self_groups
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def roles_match?(other_roles, self_roles)
|
|
210
|
+
return false unless other_roles.size == self_roles.size
|
|
211
|
+
other_roles.each_index { |idx| return false unless role_matches?(other_roles[idx], self_roles[idx]) }
|
|
212
|
+
true
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def role_matches?(role1, role2)
|
|
216
|
+
keys_with_expected_diffs = ['permissions']
|
|
217
|
+
same_num_fields = (role1.size == role2.size)
|
|
218
|
+
same_byte_length = (role1.to_s.size == role2.to_s.size)
|
|
219
|
+
same_num_fields && same_byte_length && same_role_contents?(role1, role2, keys_with_expected_diffs)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def same_role_contents?(role1, role2, keys_to_ignore)
|
|
223
|
+
role1.keys.each do |key|
|
|
224
|
+
next if keys_to_ignore.include?(key)
|
|
225
|
+
return false unless role1[key] == role2[key]
|
|
226
|
+
end
|
|
227
|
+
true
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Scooter
|
|
2
|
+
module HttpDispatchers
|
|
3
|
+
module Rbac
|
|
4
|
+
module V1
|
|
5
|
+
# Methods defined here are broken out from the rest of the RBAC
|
|
6
|
+
# endpoints because they use a ldapdispatcher object to determine
|
|
7
|
+
# various settings for the ds endpoint.
|
|
8
|
+
module DirectoryService
|
|
9
|
+
|
|
10
|
+
def ds_default_settings(ldapdispatcher)
|
|
11
|
+
base_dn_to_chomp = ',' + ldapdispatcher.base
|
|
12
|
+
user_rdn = ldapdispatcher.users_dn.chomp(base_dn_to_chomp)
|
|
13
|
+
group_rdn = ldapdispatcher.groups_dn.chomp(base_dn_to_chomp)
|
|
14
|
+
settings = {
|
|
15
|
+
"id" => 1,
|
|
16
|
+
"display_name" => 'test_ds',
|
|
17
|
+
"help_link" => 'https://example.com',
|
|
18
|
+
"hostname" => ldapdispatcher.host,
|
|
19
|
+
"port" => ldapdispatcher.port,
|
|
20
|
+
"login" => ldapdispatcher.admin_dn,
|
|
21
|
+
"password" => ldapdispatcher.return_default_password,
|
|
22
|
+
"connect_timeout" => 20,
|
|
23
|
+
"ssl" => true,
|
|
24
|
+
"base_dn" => ldapdispatcher.base,
|
|
25
|
+
"user_lookup_attr" => 'cn',
|
|
26
|
+
"user_email_attr" => 'mail',
|
|
27
|
+
"user_display_name_attr" => 'displayName',
|
|
28
|
+
"group_object_class" => '*',
|
|
29
|
+
"group_name_attr" => 'name',
|
|
30
|
+
"group_member_attr" => 'uniqueMember',
|
|
31
|
+
"group_lookup_attr" => 'cn',
|
|
32
|
+
"user_rdn" => user_rdn,
|
|
33
|
+
"group_rdn" => group_rdn
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Change the group_member_attr to just member if windows is the
|
|
37
|
+
# directory service, because AD doesn't support the attribute
|
|
38
|
+
# uniqueMember
|
|
39
|
+
settings['group_member_attr'] = 'member' if ldapdispatcher.is_windows_ad?
|
|
40
|
+
settings
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def attach_ds_to_rbac(ldapdispatcher=nil, options={})
|
|
44
|
+
settings = ldapdispatcher ? ds_default_settings(ldapdispatcher) : {}
|
|
45
|
+
settings.merge!(options)
|
|
46
|
+
|
|
47
|
+
set_rbac_path
|
|
48
|
+
@connection.put('v1/ds') do |req|
|
|
49
|
+
req.body = settings
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_attach_ds_to_rbac(ldapdispatcher=nil, options={})
|
|
54
|
+
settings = ldapdispatcher ? ds_default_settings(ldapdispatcher) : {}
|
|
55
|
+
settings.merge!(options)
|
|
56
|
+
|
|
57
|
+
set_rbac_path
|
|
58
|
+
@connection.put('v1/ds/test') do |req|
|
|
59
|
+
req.body = settings
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
%w( directory_service ).each do |lib|
|
|
2
|
+
require "scooter/httpdispatchers/rbac/v1/#{lib}"
|
|
3
|
+
end
|
|
4
|
+
module Scooter
|
|
5
|
+
module HttpDispatchers
|
|
6
|
+
module Rbac
|
|
7
|
+
# Methods here are generally representative of endpoints, and depending
|
|
8
|
+
# on the method, return either a Faraday response object or some sort of
|
|
9
|
+
# instance of the object created/modified.
|
|
10
|
+
module V1
|
|
11
|
+
|
|
12
|
+
include Scooter::HttpDispatchers::Rbac::V1::DirectoryService
|
|
13
|
+
|
|
14
|
+
def create_local_user(options)
|
|
15
|
+
set_rbac_path
|
|
16
|
+
@connection.post 'v1/users' do |request|
|
|
17
|
+
request.body = options
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def update_local_user(update_hash)
|
|
22
|
+
set_rbac_path
|
|
23
|
+
@connection.put "v1/users/#{update_hash['id']}" do |request|
|
|
24
|
+
request.body = update_hash
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def delete_local_user(user_id)
|
|
29
|
+
set_rbac_path
|
|
30
|
+
@connection.delete "v1/users/#{user_id}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_single_user_data(uuid)
|
|
34
|
+
set_rbac_path
|
|
35
|
+
@connection.get("v1/users/#{uuid}").env.body
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def get_current_user_data
|
|
39
|
+
set_rbac_path
|
|
40
|
+
@connection.get('v1/users/current').env.body
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The ParseJson middleware throws an exception because this returns
|
|
44
|
+
# json headers while simply returning a token. In order to avoid this
|
|
45
|
+
# middleware throwing an error, we catch the exception and parse the
|
|
46
|
+
# ParsingError object for a token in the error message.
|
|
47
|
+
def create_password_reset_token(uuid)
|
|
48
|
+
set_rbac_path
|
|
49
|
+
begin
|
|
50
|
+
token = @connection.post("v1/users/#{uuid}/password/reset").env.body
|
|
51
|
+
rescue Faraday::ParsingError => error
|
|
52
|
+
# Use a regex to parse the token from the ParsingError object
|
|
53
|
+
regex = /\'(.+)\'/
|
|
54
|
+
token = regex.match(error.message)[1]
|
|
55
|
+
end
|
|
56
|
+
token
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def get_list_of_users
|
|
60
|
+
set_rbac_path
|
|
61
|
+
@connection.get('v1/users').env.body
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def get_list_of_groups
|
|
65
|
+
set_rbac_path
|
|
66
|
+
@connection.get('v1/groups').env.body
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def import_ldap_group(group_name, role_ids=[])
|
|
70
|
+
set_rbac_path
|
|
71
|
+
@connection.post('v1/groups') do |request|
|
|
72
|
+
request.body = {'login' => group_name,
|
|
73
|
+
'role_ids' => role_ids}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def get_list_of_roles
|
|
78
|
+
set_rbac_path
|
|
79
|
+
@connection.get('v1/roles').env.body
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def create_role(options)
|
|
83
|
+
set_rbac_path
|
|
84
|
+
@connection.post('v1/roles') do |request|
|
|
85
|
+
request.body = options
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def replace_role(role)
|
|
90
|
+
set_rbac_path
|
|
91
|
+
@connection.put("v1/roles/#{role['id']}") do |request|
|
|
92
|
+
request.body = role
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def delete_role(role_id)
|
|
97
|
+
set_rbac_path
|
|
98
|
+
@connection.delete("v1/roles/#{role_id}")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def acquire_token(login, password, lifetime=nil)
|
|
102
|
+
set_rbac_path
|
|
103
|
+
response = @connection.post "v1/auth/token" do |request|
|
|
104
|
+
creds= {}
|
|
105
|
+
creds[:login] = login
|
|
106
|
+
creds[:password] = password
|
|
107
|
+
creds[:lifetime] = lifetime if lifetime
|
|
108
|
+
request.body = creds
|
|
109
|
+
end
|
|
110
|
+
response.env.body['token']
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/scooter/ldap.rb
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
%w( ldap_fixtures ).each do |lib|
|
|
2
|
+
require "scooter/ldap/#{lib}"
|
|
3
|
+
end
|
|
4
|
+
module Scooter
|
|
5
|
+
module LDAP
|
|
6
|
+
|
|
7
|
+
DEFAULT_DS_PORT = 636
|
|
8
|
+
DEFAULT_USER_PASSWORD = 'Puppet11'
|
|
9
|
+
|
|
10
|
+
# == Quick-start guide for the impatient
|
|
11
|
+
# === Quick example creating an LDAPDispatcher object with a beaker config:
|
|
12
|
+
#
|
|
13
|
+
# #Example beaker configuration
|
|
14
|
+
# #
|
|
15
|
+
# #HOSTS:
|
|
16
|
+
# # win_2008_r2_x64:
|
|
17
|
+
# # roles:
|
|
18
|
+
# # - directory_service
|
|
19
|
+
# # platform: windows-2008r2-x86_64
|
|
20
|
+
#
|
|
21
|
+
# require 'scooter'
|
|
22
|
+
# ldapdispatcher = Scooter::LDAP::LDAPDispatcher.new(directory_service)
|
|
23
|
+
# # If you are not using the default static fixtures, you probably want
|
|
24
|
+
# # to change the credentials for your LDAP instance
|
|
25
|
+
# ldapdispatcher.auth(user_dn, password)
|
|
26
|
+
# # This is the normal method you would use to set up a standard test
|
|
27
|
+
# # environment, with groups of writers(poets, lyricists, novelists)
|
|
28
|
+
# # to populate your directory_service
|
|
29
|
+
# ldapdispatcher.create_default_test_groups_and_users
|
|
30
|
+
class LDAPDispatcher < Net::LDAP
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
attr_accessor :test_uid
|
|
34
|
+
attr_reader :ds_type, :users_dn, :groups_dn, :ds_users
|
|
35
|
+
|
|
36
|
+
# Instantiating an object of type Scooter::LDAP::LDAPDispatcher extends
|
|
37
|
+
# the Net::LDAP class to include helper methods to make test setup and
|
|
38
|
+
# cleanup more consistent and reliable for beaker tests involving the
|
|
39
|
+
# Puppet RBAC Service. LDAPDispatcher objects require either a Unix::Host
|
|
40
|
+
# or Windows::Host object passed in as a parameter, which will dictate how
|
|
41
|
+
# helper methods construct a test environment of groups and users.
|
|
42
|
+
#
|
|
43
|
+
# Unlike the Net::LDAP, LDAPDispatcher <i>does</i> test the network
|
|
44
|
+
# connection during initialization and raises a warning if it fails.
|
|
45
|
+
# @param host [Unix::Host, Windows::Host] the DS host object defined in
|
|
46
|
+
# your Beaker config
|
|
47
|
+
# @param options [hash] any params you would like to override; you are
|
|
48
|
+
# likely to want to do this if you are not using the static Puppet LDAP
|
|
49
|
+
# fixtures
|
|
50
|
+
def initialize(host, options={})
|
|
51
|
+
|
|
52
|
+
# All initialized LDAPDispatcher objects will have test_uids to ensure
|
|
53
|
+
# no collisions when creating entries in the directory services.
|
|
54
|
+
@test_uid = Scooter::Utilities::RandomString.generate(4)
|
|
55
|
+
if host.is_a? Windows::Host
|
|
56
|
+
@ds_type = :ad
|
|
57
|
+
elsif host.is_a? Unix::Host
|
|
58
|
+
@ds_type = :openldap
|
|
59
|
+
else
|
|
60
|
+
raise "host must be Unix::Host or Windows::Host, not #{host.class}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
generated_args = {}
|
|
64
|
+
generated_args[:host] = host.reachable_name
|
|
65
|
+
generated_args[:port] = DEFAULT_DS_PORT
|
|
66
|
+
generated_args[:encryption] = {:method => :simple_tls}
|
|
67
|
+
generated_args[:base] = return_default_base
|
|
68
|
+
|
|
69
|
+
generated_args.merge!(options)
|
|
70
|
+
super(generated_args)
|
|
71
|
+
|
|
72
|
+
# If we didn't pass in an :auth hash, generate the default settings
|
|
73
|
+
# using the auth method of Net::LDAP
|
|
74
|
+
if !options[:auth]
|
|
75
|
+
self.auth admin_dn, return_default_password
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if !bind
|
|
79
|
+
warn "Problem binding to #{host}, #{get_operation_result}\n
|
|
80
|
+
username: #{admin_dn}, pw: #{return_default_password}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def return_default_password
|
|
85
|
+
"Puppet11"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def return_default_base
|
|
89
|
+
'dc=delivery,dc=puppetlabs,dc=net'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def is_openldap?
|
|
93
|
+
true if @ds_type == :openldap
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def is_windows_ad?
|
|
97
|
+
true if @ds_type == :ad
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def admin_dn
|
|
101
|
+
if is_windows_ad?
|
|
102
|
+
"cn=Administrator,cn=Users,#{return_default_base}"
|
|
103
|
+
else
|
|
104
|
+
"cn=admin,#{return_default_base}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def create_temp_ou(base_string='test_')
|
|
109
|
+
ou = base_string + @test_uid
|
|
110
|
+
dn = "ou=#{ou},#{self.base}"
|
|
111
|
+
attr = {:objectClass => ['top', 'organizationalUnit'],
|
|
112
|
+
:ou => ou}
|
|
113
|
+
add(:dn => dn, :attributes => attr)
|
|
114
|
+
|
|
115
|
+
if get_operation_result.code != 0
|
|
116
|
+
raise "OU creation failed: #{get_operation_result}, #{dn}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
dn
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# This method should execute after a test's completion; the group's ou
|
|
123
|
+
# and users's ou will be deleted, as will any entity with those
|
|
124
|
+
# respective ou's in their distinguished name.
|
|
125
|
+
# === Example beaker teardown
|
|
126
|
+
#
|
|
127
|
+
# Example beaker teardown
|
|
128
|
+
#
|
|
129
|
+
# teardown do
|
|
130
|
+
# ldapdispatcher.delete_users_and_groups_organizational_units
|
|
131
|
+
# end
|
|
132
|
+
def delete_users_and_groups_organizational_units
|
|
133
|
+
delete_all_dn_entries(@groups_dn)
|
|
134
|
+
delete_all_dn_entries(@users_dn)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def delete_all_dn_entries(dn)
|
|
138
|
+
entries = search(:base => dn, :attributes => ['dn'])
|
|
139
|
+
entries.each do |entry|
|
|
140
|
+
delete :dn => entry.dn
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
#This needs to be repeated because it may have failed deleting a group
|
|
144
|
+
#that still had users associated.
|
|
145
|
+
entries.each do |entry|
|
|
146
|
+
delete :dn => entry.dn
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
#This request should return nil; all entities with the dn provided
|
|
150
|
+
#should now be deleted.
|
|
151
|
+
entries = search(:base => dn, :attributes => ['dn'])
|
|
152
|
+
if entries != nil
|
|
153
|
+
raise "Problem deleting all entries for this dn: #{dn}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def create_ds_user(attributes, users_dn=self.users_dn)
|
|
158
|
+
default_attributes = {:objectClass => ['top',
|
|
159
|
+
'person',
|
|
160
|
+
'organizationalPerson',
|
|
161
|
+
'inetOrgPerson']}
|
|
162
|
+
|
|
163
|
+
if is_windows_ad?
|
|
164
|
+
default_attributes[:userAccountControl] = ['544']
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
default_attributes.merge!(attributes)
|
|
168
|
+
|
|
169
|
+
add(:dn => "cn=#{default_attributes[:cn]},#{users_dn}",
|
|
170
|
+
:attributes => default_attributes)
|
|
171
|
+
|
|
172
|
+
if get_operation_result.code != 0
|
|
173
|
+
raise "Creating user failed: #{get_operation_result}\n
|
|
174
|
+
#{default_attributes}"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def create_ds_group(attributes, groups_dn=self.groups_dn)
|
|
179
|
+
|
|
180
|
+
#When Openldap, you must specify :member entries in the attributes
|
|
181
|
+
default_attributes = {:objectClass => ["top", "groupOfUniqueNames"]}
|
|
182
|
+
|
|
183
|
+
if is_windows_ad?
|
|
184
|
+
default_attributes[:objectClass] = ["top", "group"]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
default_attributes.merge!(attributes)
|
|
188
|
+
|
|
189
|
+
add(:dn => "cn=#{default_attributes[:cn]},#{groups_dn}",
|
|
190
|
+
:attributes => default_attributes)
|
|
191
|
+
|
|
192
|
+
if get_operation_result.code != 0
|
|
193
|
+
raise "Creating group failed: #{get_operation_result},\n
|
|
194
|
+
#{default_attributes}"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def create_ou_for_users_and_groups
|
|
199
|
+
@users_dn = create_temp_ou('users')
|
|
200
|
+
@groups_dn = create_temp_ou('groups')
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# This is the primary method most tests will use. It creates two
|
|
204
|
+
# organizational units, or ou's, to base all your testing around. There is
|
|
205
|
+
# one ou for groups and one for users. Most testing can be covered by
|
|
206
|
+
# simply running the method <tt>create_default_test_groups_and_users</tt>.
|
|
207
|
+
def create_default_test_groups_and_users
|
|
208
|
+
|
|
209
|
+
create_ou_for_users_and_groups
|
|
210
|
+
|
|
211
|
+
create_default_users
|
|
212
|
+
|
|
213
|
+
if is_windows_ad?
|
|
214
|
+
create_windows_ad_default_users_and_test_groups
|
|
215
|
+
elsif is_openldap?
|
|
216
|
+
create_openldap_default_users_and_test_groups
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def create_default_users
|
|
222
|
+
users = Scooter::LDAP::LDAPFixtures.users(@test_uid)
|
|
223
|
+
|
|
224
|
+
users.each do |name, hash|
|
|
225
|
+
create_ds_user(hash)
|
|
226
|
+
update_user_password("CN=#{hash[:cn]},#{users_dn}", DEFAULT_USER_PASSWORD)
|
|
227
|
+
hash[:password] = DEFAULT_USER_PASSWORD
|
|
228
|
+
end
|
|
229
|
+
@ds_users = users
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
#This is used to encode passwords for Windows AD
|
|
233
|
+
#See URL: http://msdn.microsoft.com/en-us/library/cc223248.aspx
|
|
234
|
+
def str_to_unicode_pwd(str) #:nodoc:
|
|
235
|
+
('"' + str + '"').encode("utf-16le").force_encoding("utf-8")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def update_user_password(user_dn, password) #:nodoc:
|
|
239
|
+
if is_windows_ad?
|
|
240
|
+
password = str_to_unicode_pwd(password)
|
|
241
|
+
ops = [[:replace, :unicodePwd, password]]
|
|
242
|
+
else
|
|
243
|
+
ops = [[:replace, :userPassword, password]]
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
modify :dn => user_dn, :operations => ops
|
|
247
|
+
|
|
248
|
+
if get_operation_result.code != 0
|
|
249
|
+
raise "Updating password failed: #{get_operation_result}\n
|
|
250
|
+
#{ops}"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
private
|
|
255
|
+
def create_openldap_default_users_and_test_groups #:nodoc:
|
|
256
|
+
|
|
257
|
+
#add novelists
|
|
258
|
+
novelists =["cn=#{ds_users['arthur'][:cn]},#{users_dn}",
|
|
259
|
+
"cn=#{ds_users['howard'][:cn]},#{users_dn}",
|
|
260
|
+
"cn=#{ds_users['oscar'][:cn]},#{users_dn}"]
|
|
261
|
+
|
|
262
|
+
create_ds_group({ :cn => "novelists#{@test_uid}",
|
|
263
|
+
:uniqueMember => novelists })
|
|
264
|
+
|
|
265
|
+
#add poets
|
|
266
|
+
poets = ["cn=#{ds_users['sylvia'][:cn]},#{users_dn}",
|
|
267
|
+
"cn=#{ds_users['jorge'][:cn]},#{users_dn}",
|
|
268
|
+
"cn=#{ds_users['oscar'][:cn]},#{users_dn}"]
|
|
269
|
+
|
|
270
|
+
create_ds_group({ :cn => "poets#{@test_uid}",
|
|
271
|
+
:uniqueMember => poets })
|
|
272
|
+
|
|
273
|
+
#add lyricists
|
|
274
|
+
lyricists = ["cn=#{ds_users['stephen'][:cn]},#{users_dn}"]
|
|
275
|
+
|
|
276
|
+
create_ds_group({ :cn => "lyricists#{@test_uid}",
|
|
277
|
+
:uniqueMember => lyricists })
|
|
278
|
+
|
|
279
|
+
#add writers
|
|
280
|
+
writers = ["cn=#{ds_users['guillermo'][:cn]},#{users_dn}",
|
|
281
|
+
"cn=novelists#{test_uid},#{groups_dn}",
|
|
282
|
+
"cn=poets#{test_uid},#{groups_dn}",
|
|
283
|
+
"cn=lyricists#{test_uid},#{groups_dn}"]
|
|
284
|
+
|
|
285
|
+
create_ds_group({ :cn => "writers#{@test_uid}",
|
|
286
|
+
:uniqueMember => writers })
|
|
287
|
+
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def create_windows_ad_default_users_and_test_groups #:nodoc:
|
|
291
|
+
|
|
292
|
+
writers_cn = "writers#{@test_uid}"
|
|
293
|
+
poets_cn = "poets#{@test_uid}"
|
|
294
|
+
novelists_cn = "novelists#{@test_uid}"
|
|
295
|
+
lyricists_cn = "lyricists#{@test_uid}"
|
|
296
|
+
|
|
297
|
+
create_ds_group(:cn => lyricists_cn)
|
|
298
|
+
create_ds_group(:cn => novelists_cn)
|
|
299
|
+
create_ds_group(:cn => poets_cn)
|
|
300
|
+
create_ds_group(:cn => writers_cn)
|
|
301
|
+
|
|
302
|
+
add_attribute("cn=#{writers_cn},#{groups_dn}",
|
|
303
|
+
:member,
|
|
304
|
+
"cn=#{lyricists_cn},#{groups_dn}")
|
|
305
|
+
|
|
306
|
+
add_attribute("cn=#{writers_cn},#{groups_dn}",
|
|
307
|
+
:member,
|
|
308
|
+
"cn=#{poets_cn},#{groups_dn}")
|
|
309
|
+
|
|
310
|
+
add_attribute("cn=#{writers_cn},#{groups_dn}",
|
|
311
|
+
:member,
|
|
312
|
+
"cn=#{novelists_cn},#{groups_dn}")
|
|
313
|
+
|
|
314
|
+
add_attribute("cn=#{novelists_cn},#{groups_dn}",
|
|
315
|
+
:member,
|
|
316
|
+
"cn=#{ds_users['howard'][:cn]},#{users_dn}")
|
|
317
|
+
|
|
318
|
+
add_attribute("cn=#{novelists_cn},#{groups_dn}",
|
|
319
|
+
:member,
|
|
320
|
+
"cn=#{ds_users['arthur'][:cn]},#{users_dn}")
|
|
321
|
+
|
|
322
|
+
add_attribute("cn=#{novelists_cn},#{groups_dn}",
|
|
323
|
+
:member,
|
|
324
|
+
"cn=#{ds_users['oscar'][:cn]},#{users_dn}")
|
|
325
|
+
|
|
326
|
+
add_attribute("cn=#{poets_cn},#{groups_dn}",
|
|
327
|
+
:member,
|
|
328
|
+
"cn=#{ds_users['sylvia'][:cn]},#{users_dn}")
|
|
329
|
+
|
|
330
|
+
add_attribute("cn=#{poets_cn},#{groups_dn}",
|
|
331
|
+
:member,
|
|
332
|
+
"cn=#{ds_users['jorge'][:cn]},#{users_dn}")
|
|
333
|
+
|
|
334
|
+
add_attribute("cn=#{poets_cn},#{groups_dn}",
|
|
335
|
+
:member,
|
|
336
|
+
"cn=#{ds_users['oscar'][:cn]},#{users_dn}")
|
|
337
|
+
|
|
338
|
+
add_attribute("cn=#{writers_cn},#{groups_dn}",
|
|
339
|
+
:member,
|
|
340
|
+
"cn=#{ds_users['guillermo'][:cn]},#{users_dn}")
|
|
341
|
+
|
|
342
|
+
add_attribute("cn=#{lyricists_cn},#{groups_dn}",
|
|
343
|
+
:member,
|
|
344
|
+
"cn=#{ds_users['stephen'][:cn]},#{users_dn}")
|
|
345
|
+
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|