entitlements-app 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/VERSION +1 -0
- data/bin/deploy-entitlements +18 -0
- data/lib/entitlements/auditor/base.rb +163 -0
- data/lib/entitlements/backend/base_controller.rb +171 -0
- data/lib/entitlements/backend/base_provider.rb +55 -0
- data/lib/entitlements/backend/dummy/controller.rb +89 -0
- data/lib/entitlements/backend/dummy.rb +3 -0
- data/lib/entitlements/backend/ldap/controller.rb +188 -0
- data/lib/entitlements/backend/ldap/provider.rb +128 -0
- data/lib/entitlements/backend/ldap.rb +4 -0
- data/lib/entitlements/backend/member_of/controller.rb +203 -0
- data/lib/entitlements/backend/member_of.rb +3 -0
- data/lib/entitlements/cli.rb +121 -0
- data/lib/entitlements/data/groups/cached.rb +120 -0
- data/lib/entitlements/data/groups/calculated/base.rb +478 -0
- data/lib/entitlements/data/groups/calculated/filters/base.rb +93 -0
- data/lib/entitlements/data/groups/calculated/filters/member_of_group.rb +32 -0
- data/lib/entitlements/data/groups/calculated/modifiers/base.rb +38 -0
- data/lib/entitlements/data/groups/calculated/modifiers/expiration.rb +56 -0
- data/lib/entitlements/data/groups/calculated/ruby.rb +137 -0
- data/lib/entitlements/data/groups/calculated/rules/base.rb +35 -0
- data/lib/entitlements/data/groups/calculated/rules/group.rb +129 -0
- data/lib/entitlements/data/groups/calculated/rules/username.rb +41 -0
- data/lib/entitlements/data/groups/calculated/text.rb +337 -0
- data/lib/entitlements/data/groups/calculated/yaml.rb +171 -0
- data/lib/entitlements/data/groups/calculated.rb +290 -0
- data/lib/entitlements/data/groups.rb +13 -0
- data/lib/entitlements/data/people/combined.rb +197 -0
- data/lib/entitlements/data/people/dummy.rb +71 -0
- data/lib/entitlements/data/people/ldap.rb +142 -0
- data/lib/entitlements/data/people/yaml.rb +102 -0
- data/lib/entitlements/data/people.rb +58 -0
- data/lib/entitlements/extras/base.rb +40 -0
- data/lib/entitlements/extras/ldap_group/base.rb +20 -0
- data/lib/entitlements/extras/ldap_group/filters/member_of_ldap_group.rb +50 -0
- data/lib/entitlements/extras/ldap_group/rules/ldap_group.rb +69 -0
- data/lib/entitlements/extras/orgchart/base.rb +32 -0
- data/lib/entitlements/extras/orgchart/logic.rb +171 -0
- data/lib/entitlements/extras/orgchart/person_methods.rb +55 -0
- data/lib/entitlements/extras/orgchart/rules/direct_report.rb +62 -0
- data/lib/entitlements/extras/orgchart/rules/management.rb +59 -0
- data/lib/entitlements/extras.rb +82 -0
- data/lib/entitlements/models/action.rb +82 -0
- data/lib/entitlements/models/group.rb +280 -0
- data/lib/entitlements/models/person.rb +149 -0
- data/lib/entitlements/plugins/dummy.rb +22 -0
- data/lib/entitlements/plugins/group_of_names.rb +28 -0
- data/lib/entitlements/plugins/posix_group.rb +46 -0
- data/lib/entitlements/plugins.rb +13 -0
- data/lib/entitlements/rule/base.rb +74 -0
- data/lib/entitlements/service/ldap.rb +405 -0
- data/lib/entitlements/util/mirror.rb +42 -0
- data/lib/entitlements/util/override.rb +64 -0
- data/lib/entitlements/util/util.rb +219 -0
- data/lib/entitlements.rb +606 -0
- metadata +343 -0
@@ -0,0 +1,405 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/ldap"
|
4
|
+
|
5
|
+
module Entitlements
|
6
|
+
class Service
|
7
|
+
class LDAP
|
8
|
+
include ::Contracts::Core
|
9
|
+
C = ::Contracts
|
10
|
+
|
11
|
+
class ConnectionError < RuntimeError; end
|
12
|
+
class DuplicateEntryError < RuntimeError; end
|
13
|
+
class EntryError < RuntimeError; end
|
14
|
+
class WTFError < RuntimeError; end
|
15
|
+
|
16
|
+
# We use the binddn as the owner of the group, for lack of anything better.
|
17
|
+
# This keeps the schema happy.
|
18
|
+
attr_reader :binddn, :person_dn_format
|
19
|
+
|
20
|
+
# Constructor-like object that ensures only one LDAP object (and hence connection)
|
21
|
+
# is made for a given LDAP server, for efficiency sake. Takes the same parameters as
|
22
|
+
# the constructor and returns the same object type.
|
23
|
+
#
|
24
|
+
# addr - URL of LDAP server e.g. ldaps://ldap.example.net:636
|
25
|
+
# binddn - DN to bind with
|
26
|
+
# bindpw - Password for the bind user
|
27
|
+
# ca_file - Can be set to a CA certificate
|
28
|
+
# disable_ssl_verification - Can be set to true to disable SSL verification when connecting
|
29
|
+
#
|
30
|
+
# Returns Entitlements::Service::LDAP object.
|
31
|
+
Contract C::KeywordArgs[
|
32
|
+
addr: String,
|
33
|
+
binddn: String,
|
34
|
+
bindpw: String,
|
35
|
+
ca_file: C::Optional[C::Or[nil, String]],
|
36
|
+
disable_ssl_verification: C::Optional[C::Bool],
|
37
|
+
person_dn_format: String
|
38
|
+
] => Entitlements::Service::LDAP
|
39
|
+
def self.new_with_cache(addr:, binddn:, bindpw:, ca_file: ENV["LDAP_CACERT"], disable_ssl_verification: false, person_dn_format:)
|
40
|
+
# only look at LDAP_DISABLE_SSL_VERIFICATION in the environment if we didn't pass true to the method already
|
41
|
+
if disable_ssl_verification == false
|
42
|
+
# otherwise if it's set to anything at all in env, disable ssl verification
|
43
|
+
disable_ssl_verification = !!ENV["LDAP_DISABLE_SSL_VERIFICATION"]
|
44
|
+
end
|
45
|
+
fingerprint = [addr, binddn, bindpw, ca_file, disable_ssl_verification, person_dn_format].map(&:inspect).join("|")
|
46
|
+
Entitlements.cache[:ldap_connections] ||= {}
|
47
|
+
Entitlements.cache[:ldap_connections][fingerprint] ||= new(
|
48
|
+
addr: addr,
|
49
|
+
binddn: binddn,
|
50
|
+
bindpw: bindpw,
|
51
|
+
ca_file: ca_file,
|
52
|
+
disable_ssl_verification: disable_ssl_verification,
|
53
|
+
person_dn_format: person_dn_format
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Constructor.
|
58
|
+
#
|
59
|
+
# addr - URL of LDAP server e.g. ldaps://ldap.example.net:636
|
60
|
+
# binddn - DN to bind with
|
61
|
+
# bindpw - Password for the bind user
|
62
|
+
# ca_file - Can be set to a CA certificate
|
63
|
+
# disable_ssl_verification - Can be set to true to disable SSL verification when connecting
|
64
|
+
# person_dn_format - String template to convert a bare username to a distinguished name (`%KEY%` is replaced)
|
65
|
+
#
|
66
|
+
# Returns nothing.
|
67
|
+
Contract C::KeywordArgs[
|
68
|
+
addr: String,
|
69
|
+
binddn: String,
|
70
|
+
bindpw: String,
|
71
|
+
ca_file: C::Optional[C::Or[nil, String]],
|
72
|
+
disable_ssl_verification: C::Optional[C::Bool],
|
73
|
+
person_dn_format: String
|
74
|
+
] => C::Any
|
75
|
+
def initialize(addr:, binddn:, bindpw:, ca_file: ENV["LDAP_CACERT"], disable_ssl_verification: false, person_dn_format:)
|
76
|
+
# Save some parameters for the LDAP connection but don't actually bind yet.
|
77
|
+
@addr = addr
|
78
|
+
@binddn = binddn
|
79
|
+
@bindpw = bindpw
|
80
|
+
@ca_file = ca_file
|
81
|
+
@disable_ssl_verification = disable_ssl_verification
|
82
|
+
@person_dn_format = person_dn_format
|
83
|
+
end
|
84
|
+
|
85
|
+
# Read a single entry identified by its DN and return the value. Returns nil if
|
86
|
+
# the entry does not exist.
|
87
|
+
#
|
88
|
+
# dn - A String with the distinguished name
|
89
|
+
#
|
90
|
+
# Returns the Net::LDAP::Entry if it exists, nil otherwise.
|
91
|
+
Contract String => C::Or[nil, Net::LDAP::Entry]
|
92
|
+
def read(dn)
|
93
|
+
@dn_cache ||= {}
|
94
|
+
@dn_cache[dn] ||= search(base: dn, attrs: "*", scope: Net::LDAP::SearchScope_BaseObject)[dn] || :none
|
95
|
+
@dn_cache[dn] == :none ? nil : @dn_cache[dn]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Perform a search, iterate through each entry, and return a hash of the indexed attribute
|
99
|
+
# to the results.
|
100
|
+
#
|
101
|
+
# base - A String with the base of the search
|
102
|
+
# filter - A Net::LDAP::Filter object with the search. Leave undefined for no filter.
|
103
|
+
# attrs - An Array of Strings with the attributes to retrieve. Can also be just "*".
|
104
|
+
# index - A String with the attribute to build the hash table on (default: dn)
|
105
|
+
#
|
106
|
+
# Returns a Hash of entries (entries are hashes if multiple == false, Arrays of hashes if multiple == true)
|
107
|
+
Contract C::KeywordArgs[
|
108
|
+
base: String,
|
109
|
+
filter: C::Maybe[Net::LDAP::Filter],
|
110
|
+
attrs: C::Maybe[C::Or[C::ArrayOf[String], "*"]],
|
111
|
+
index: C::Maybe[C::Or[Symbol, String]],
|
112
|
+
scope: C::Maybe[Integer]
|
113
|
+
] => C::HashOf[String => C::Or[Net::LDAP::Entry, C::ArrayOf[Net::LDAP::Entry]]]
|
114
|
+
def search(base:, filter: nil, attrs: "*", index: :dn, scope: Net::LDAP::SearchScope_WholeSubtree)
|
115
|
+
Entitlements.logger.debug "LDAP Search: filter=#{filter.inspect} base=#{base.inspect}"
|
116
|
+
|
117
|
+
# Ruby downcases these in the results anyway, so just downcase everything here so it'll
|
118
|
+
# be consistent no matter what. LDAP is case insensitive after all!
|
119
|
+
downcased_attrs = attrs == "*" ? "*" : attrs.map { |a| a.downcase }
|
120
|
+
|
121
|
+
result = {}
|
122
|
+
ldap.search(base: base, filter: filter, attributes: downcased_attrs, scope: scope, return_result: false) do |entry|
|
123
|
+
result_key = index == :dn ? entry.dn : entry[index]
|
124
|
+
unless result_key
|
125
|
+
raise EntryError, "#{entry.dn} has no value for #{index.inspect}"
|
126
|
+
end
|
127
|
+
|
128
|
+
if result.key?(result_key)
|
129
|
+
other_entry_dn = result[result_key].dn
|
130
|
+
raise DuplicateEntryError, "#{entry.dn} and #{other_entry_dn} have the same value of #{index} = #{result_key.inspect}"
|
131
|
+
end
|
132
|
+
|
133
|
+
result[result_key] = entry
|
134
|
+
end
|
135
|
+
|
136
|
+
Entitlements.logger.debug "Completed search: #{result.keys.size} result(s)"
|
137
|
+
|
138
|
+
result
|
139
|
+
end
|
140
|
+
|
141
|
+
# Determine if an entry exists, and return true or false.
|
142
|
+
#
|
143
|
+
# dn - A String with the distinguished name
|
144
|
+
#
|
145
|
+
# Returns true if the entry exists, false otherwise.
|
146
|
+
Contract String => C::Bool
|
147
|
+
def exists?(dn)
|
148
|
+
read(dn).is_a?(Net::LDAP::Entry)
|
149
|
+
end
|
150
|
+
|
151
|
+
# "Upsert" -- update or create an entry in LDAP.
|
152
|
+
#
|
153
|
+
# dn - A String with the distinguished name
|
154
|
+
# attributes - Hash that defines the values to be set
|
155
|
+
#
|
156
|
+
# Returns true if it succeeded, false if it did not.
|
157
|
+
Contract C::KeywordArgs[
|
158
|
+
dn: String,
|
159
|
+
attributes: C::HashOf[String => C::Any],
|
160
|
+
] => C::Or[C::Bool, nil]
|
161
|
+
def upsert(dn:, attributes:)
|
162
|
+
# See if the object exists by searching for it. If it exists we'll get its data back as a hash. If not
|
163
|
+
# we'll get an empty hash. Dispatch this to the create or update methods.
|
164
|
+
read(dn) ? update(dn: dn, existing: read(dn), attributes: attributes) : create(dn: dn, attributes: attributes)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Delete an entry in LDAP.
|
168
|
+
#
|
169
|
+
# dn - A String with the distinguished name
|
170
|
+
#
|
171
|
+
# Returns true if it succeeded, false if it did not.
|
172
|
+
Contract String => C::Bool
|
173
|
+
def delete(dn)
|
174
|
+
# See if the object exists by searching for it. If it exists we'll get its data back as a hash. If not
|
175
|
+
# we'll get an empty hash. We don't need to delete something that doesn't already exist.
|
176
|
+
unless exists?(dn)
|
177
|
+
Entitlements.logger.debug "Not deleting #{dn} because it does not exist"
|
178
|
+
return true
|
179
|
+
end
|
180
|
+
|
181
|
+
ldap.delete(dn: dn)
|
182
|
+
operation_result = ldap.get_operation_result
|
183
|
+
return true if operation_result["code"] == 0
|
184
|
+
Entitlements.logger.error "Error deleting #{dn}: #{operation_result['message']}"
|
185
|
+
false
|
186
|
+
end
|
187
|
+
|
188
|
+
# Modify an entry in LDAP. Set a value of `nil` to remove the entry instead of updating it.
|
189
|
+
#
|
190
|
+
# dn - A String with the distinguished name
|
191
|
+
# updates - A Hash of { "attribute_name" => <String>|<Array>|nil }
|
192
|
+
#
|
193
|
+
# Returns true if it succeeded, false if it did not.
|
194
|
+
Contract String, C::HashOf[String => C::Or[String, C::ArrayOf[String], nil]] => C::Bool
|
195
|
+
def modify(dn, updates)
|
196
|
+
return false unless updates.any?
|
197
|
+
updates.each do |attr_name, val|
|
198
|
+
operation = ""
|
199
|
+
if val.nil?
|
200
|
+
next if ldap.delete_attribute(dn, attr_name)
|
201
|
+
operation = "deleting"
|
202
|
+
else
|
203
|
+
next if ldap.replace_attribute(dn, attr_name, val)
|
204
|
+
operation = "modifying"
|
205
|
+
end
|
206
|
+
operation_result = ldap.get_operation_result
|
207
|
+
Entitlements.logger.error "Error #{operation} attribute #{attr_name} in #{dn}: #{operation_result['message']}"
|
208
|
+
Entitlements.logger.error "LDAP code=#{operation_result.code}: #{operation_result.error_message}"
|
209
|
+
return false
|
210
|
+
end
|
211
|
+
true
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
attr_reader :addr, :bindpw
|
217
|
+
|
218
|
+
# The LDAP object is initialized and bound on demand the first time it's called.
|
219
|
+
#
|
220
|
+
# Takes no arguments.
|
221
|
+
#
|
222
|
+
# Returns a Net::LDAP object that is connected and bound.
|
223
|
+
Contract C::None => Net::LDAP
|
224
|
+
def ldap
|
225
|
+
@ldap ||= begin
|
226
|
+
uri = URI(addr)
|
227
|
+
|
228
|
+
# Construct the object
|
229
|
+
Entitlements.logger.debug "Creating connection to #{uri.host} port #{uri.port}"
|
230
|
+
ldap_options = {
|
231
|
+
host: uri.host,
|
232
|
+
port: uri.port,
|
233
|
+
auth: { method: :simple, username: binddn, password: bindpw }
|
234
|
+
}
|
235
|
+
if uri.scheme == "ldaps"
|
236
|
+
ldap_options[:encryption] = {
|
237
|
+
method: :simple_tls,
|
238
|
+
tls_options: {
|
239
|
+
verify_mode: @disable_ssl_verification ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
|
240
|
+
}
|
241
|
+
}
|
242
|
+
else
|
243
|
+
ldap_options[:encryption] = { method: nil }
|
244
|
+
end
|
245
|
+
|
246
|
+
if @ca_file && ldap_options[:encryption].key?(:tls_options)
|
247
|
+
ldap_options[:encryption][:tls_options][:ca_file] = @ca_file
|
248
|
+
end
|
249
|
+
|
250
|
+
ldap_object = Net::LDAP.new(ldap_options)
|
251
|
+
raise WTFError, "FATAL: can't create LDAP connection object" if ldap_object.nil?
|
252
|
+
|
253
|
+
# Bind to LDAP
|
254
|
+
Entitlements.logger.debug "Binding with user #{binddn.inspect} with simple password authentication"
|
255
|
+
ldap_object.bind
|
256
|
+
operation_result = ldap_object.get_operation_result
|
257
|
+
if operation_result["code"] != 0
|
258
|
+
Entitlements.logger.fatal operation_result["message"]
|
259
|
+
raise ConnectionError, "FATAL: #{operation_result['message']}"
|
260
|
+
end
|
261
|
+
Entitlements.logger.debug "Successfully authenticated to #{uri.host} port #{uri.port}"
|
262
|
+
|
263
|
+
# Return the object itself
|
264
|
+
ldap_object
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Create an object that does not exist. Private method intended to be called from
|
269
|
+
# "upsert" after determining that the object doesn't exist.
|
270
|
+
#
|
271
|
+
# dn - A String with the distinguished name
|
272
|
+
# attributes - Hash that defines the values to be set
|
273
|
+
#
|
274
|
+
# Returns true if success, false otherwise.
|
275
|
+
Contract C::KeywordArgs[
|
276
|
+
dn: String,
|
277
|
+
attributes: C::HashOf[String => C::Any],
|
278
|
+
] => C::Bool
|
279
|
+
def create(dn:, attributes:)
|
280
|
+
ldap.add(dn: dn, attributes: attributes)
|
281
|
+
operation_result = ldap.get_operation_result
|
282
|
+
return true if operation_result["code"] == 0
|
283
|
+
if operation_result["error_message"]
|
284
|
+
Entitlements.logger.error "#{dn}: #{operation_result['code']} #{operation_result['error_message']}"
|
285
|
+
end
|
286
|
+
Entitlements.logger.error "Error creating #{dn} (#{attributes.inspect}): #{operation_result['message']}"
|
287
|
+
false
|
288
|
+
end
|
289
|
+
|
290
|
+
# Update an existing entry. Private method intended to be called from
|
291
|
+
# "upsert" after determining that the object does exist.
|
292
|
+
#
|
293
|
+
# dn - A String with the distinguished name
|
294
|
+
# existing - Net::LDAP::Entry of the existing object
|
295
|
+
# attributes - Hash that defines the values to be set
|
296
|
+
#
|
297
|
+
# Returns true if success, false otherwise. Returns nil if there was not actually a change.
|
298
|
+
Contract C::KeywordArgs[
|
299
|
+
dn: String,
|
300
|
+
existing: Net::LDAP::Entry,
|
301
|
+
attributes: C::HashOf[String => C::Any],
|
302
|
+
] => C::Or[C::Bool, nil]
|
303
|
+
def update(dn:, existing:, attributes:)
|
304
|
+
ops = ops_array(existing: existing, attributes: attributes)
|
305
|
+
return if ops.empty?
|
306
|
+
|
307
|
+
ldap.modify(dn: dn, operations: ops)
|
308
|
+
operation_result = ldap.get_operation_result
|
309
|
+
return true if operation_result["code"] == 0
|
310
|
+
Entitlements.logger.error "Error modifying #{dn}: #{ops.inspect} #{operation_result['message']}"
|
311
|
+
false
|
312
|
+
end
|
313
|
+
|
314
|
+
# Map a set of existing attributes and new attributes to an array of actions
|
315
|
+
# passed to the ldap.modify method. See http://www.rubydoc.info/gems/ruby-net-ldap/Net%2FLDAP:modify.
|
316
|
+
#
|
317
|
+
# existing - Net::LDAP::Entry of the existing object
|
318
|
+
# attributes - Hash that defines the values to be set
|
319
|
+
#
|
320
|
+
# Returns an array of add/replace/delete.
|
321
|
+
Contract C::KeywordArgs[
|
322
|
+
existing: Net::LDAP::Entry,
|
323
|
+
attributes: C::HashOf[String => C::Any]
|
324
|
+
] => C::ArrayOf[[Symbol, Symbol, C::Any]]
|
325
|
+
def ops_array(existing:, attributes:)
|
326
|
+
normalized_existing = existing.attribute_names.map do |attr_name|
|
327
|
+
[attr_name.to_s.downcase.to_sym, existing[attr_name]]
|
328
|
+
end.to_h
|
329
|
+
|
330
|
+
normalized_new = attributes.map { |k, v| [k.downcase.to_sym, v] }.to_h
|
331
|
+
|
332
|
+
all_attributes = normalized_existing.keys | normalized_new.keys
|
333
|
+
|
334
|
+
all_attributes.map do |attr_key|
|
335
|
+
attr_val = normalized_new[attr_key]
|
336
|
+
if attr_val.nil?
|
337
|
+
if normalized_existing.key?(attr_key) && normalized_new.key?(attr_key)
|
338
|
+
# Delete existing
|
339
|
+
[:delete, attr_key, nil]
|
340
|
+
elsif normalized_existing.key?(attr_key)
|
341
|
+
# Undefined in the new attributes, so ignore (will be removed by 'compact' call)
|
342
|
+
nil
|
343
|
+
else
|
344
|
+
# Nothing is there to delete, so ignore (will be removed by 'compact' call)
|
345
|
+
nil
|
346
|
+
end
|
347
|
+
else
|
348
|
+
if !normalized_existing.key?(attr_key)
|
349
|
+
# Key doesn't exist now, so this is an add
|
350
|
+
[:add, attr_key, attr_val]
|
351
|
+
elsif normalized_existing[attr_key].is_a?(Array) && normalized_existing[attr_key].size == 1 && normalized_existing[attr_key].first == attr_val
|
352
|
+
# This is equivalence, so do nothing (will be removed by 'compact' call)
|
353
|
+
nil
|
354
|
+
elsif normalized_existing[attr_key] != attr_val
|
355
|
+
# Replace existing
|
356
|
+
[:replace, attr_key, attr_val]
|
357
|
+
else
|
358
|
+
# Unchanged, so do nothing (will be removed by 'compact' call)
|
359
|
+
nil
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end.compact
|
363
|
+
end
|
364
|
+
|
365
|
+
# Construct an Entitlements::Models::Group from a Net::LDAP::Entry
|
366
|
+
#
|
367
|
+
# entry - The Net::LDAP::Entry
|
368
|
+
#
|
369
|
+
# Returns an Entitlements::Models::Group object.
|
370
|
+
Contract Net::LDAP::Entry => Entitlements::Models::Group
|
371
|
+
def self.entry_to_group(entry)
|
372
|
+
Entitlements::Models::Group.new(
|
373
|
+
dn: entry.dn,
|
374
|
+
members: Set.new(member_array(entry)),
|
375
|
+
description: entry[:description].is_a?(Array) ? entry[:description].first.to_s : ""
|
376
|
+
)
|
377
|
+
end
|
378
|
+
|
379
|
+
# Convert members in a Net::LDAP::Entry to a suitable array of DNs. Has to handle `uniquemember`
|
380
|
+
# for `groupOfUniqueNames` and `member` for `groupOfNames`.
|
381
|
+
#
|
382
|
+
# entry - The Net::LDAP::Entry
|
383
|
+
#
|
384
|
+
# Returns an Array of Strings with the first attribute (typically uid).
|
385
|
+
Contract Net::LDAP::Entry => C::ArrayOf[String]
|
386
|
+
def self.member_array(entry)
|
387
|
+
members = if entry[:objectclass].include?("groupOfUniqueNames")
|
388
|
+
entry[:uniquemember]
|
389
|
+
elsif entry[:objectclass].include?("groupOfNames")
|
390
|
+
entry[:member]
|
391
|
+
elsif entry[:objectclass].include?("posixGroup")
|
392
|
+
entry[:memberuid]
|
393
|
+
else
|
394
|
+
raise "Do not know how to handle objectClass = #{entry[:objectclass].inspect} for dn=#{entry.dn.inspect}!"
|
395
|
+
end
|
396
|
+
|
397
|
+
# If the group has itself as a member, take that out. That is a convention for the
|
398
|
+
# Entitlements LDAP provider only which needs to be kept internal.
|
399
|
+
members -= [entry.dn]
|
400
|
+
|
401
|
+
members.map { |dn| Entitlements::Util::Util.first_attr(dn) }
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Entitlements
|
4
|
+
class Util
|
5
|
+
class Mirror
|
6
|
+
include ::Contracts::Core
|
7
|
+
C = ::Contracts
|
8
|
+
|
9
|
+
# Validate a configuration for an OU that is a mirror. Return if the configuration
|
10
|
+
# is valid; raise an error if it is not.
|
11
|
+
#
|
12
|
+
# key - A String with the key from the entitlements configuration
|
13
|
+
#
|
14
|
+
# Returns nothing.
|
15
|
+
Contract String => nil
|
16
|
+
def self.validate_mirror!(key)
|
17
|
+
# Make sure there is not an existing file, directory, or anything else in the
|
18
|
+
# directory structure defined by this key.
|
19
|
+
begin
|
20
|
+
src = Entitlements::Util::Util.path_for_group(key)
|
21
|
+
raise ArgumentError, "#{key.inspect} is declared as a mirror OU but source #{src.inspect} exists!"
|
22
|
+
rescue Errno::ENOENT
|
23
|
+
# This is desired.
|
24
|
+
end
|
25
|
+
|
26
|
+
# Make sure the target exists.
|
27
|
+
target = Entitlements.config["groups"][key]["mirror"]
|
28
|
+
unless Entitlements.config["groups"].key?(target)
|
29
|
+
raise ArgumentError, "#{key.inspect} is declared as a mirror to a non-existing target #{target.inspect}!"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Make sure the target is not itself a mirror.
|
33
|
+
if Entitlements.config["groups"][target]["mirror"]
|
34
|
+
raise ArgumentError, "#{key.inspect} is declared as a mirror to a mirror target #{target.inspect}!"
|
35
|
+
end
|
36
|
+
|
37
|
+
# All is well
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Entitlements
|
4
|
+
class Util
|
5
|
+
class Override
|
6
|
+
include ::Contracts::Core
|
7
|
+
C = ::Contracts
|
8
|
+
|
9
|
+
# Handle override from plugin. Return hash, compatible with `upsert` method, that
|
10
|
+
# defines the necessary differences.
|
11
|
+
#
|
12
|
+
# plugin - A Hash with the plugin configuration, both filename and target class (can be nil)
|
13
|
+
# group - The Entitlements::Models::Group
|
14
|
+
# ldap - Reference to the underlying Entitlements::Service::LDAP object
|
15
|
+
#
|
16
|
+
# Returns a Hash or nil.
|
17
|
+
Contract C::Or[C::HashOf[String => String], nil], Entitlements::Models::Group, Entitlements::Service::LDAP => C::Or[nil, C::HashOf[String => C::Any]]
|
18
|
+
def self.override_hash_from_plugin(plugin, group, ldap)
|
19
|
+
return unless plugin
|
20
|
+
|
21
|
+
# Plugin hash should consist of a Hash with 2 keys: "file" the absolute path to the file or relative path compared to
|
22
|
+
# the entitlements configuration file, and "class" the class name that's contained within the file. If the filename
|
23
|
+
# has no "/" then it is treated as a built-in plugin, under the "plugins" directory within this gem.
|
24
|
+
unless plugin.key?("file")
|
25
|
+
raise ArgumentError, "plugin configuration hash must contain 'file' key"
|
26
|
+
end
|
27
|
+
|
28
|
+
file = if plugin["file"] !~ %r{/}
|
29
|
+
File.expand_path(File.join("../plugins", plugin["file"]), File.dirname(__FILE__))
|
30
|
+
elsif plugin["file"].start_with?("/")
|
31
|
+
plugin["file"]
|
32
|
+
else
|
33
|
+
File.expand_path(plugin["file"], File.dirname(Entitlements.config_file))
|
34
|
+
end
|
35
|
+
|
36
|
+
unless File.file?(file)
|
37
|
+
raise ArgumentError, "Could not locate plugin for #{plugin['file'].inspect} at #{file.inspect}"
|
38
|
+
end
|
39
|
+
|
40
|
+
unless plugin.key?("class")
|
41
|
+
raise ArgumentError, "plugin configuration hash must contain 'class' key"
|
42
|
+
end
|
43
|
+
|
44
|
+
require file
|
45
|
+
|
46
|
+
clazz = Kernel.const_get("Entitlements::Plugins::#{plugin['class']}")
|
47
|
+
|
48
|
+
unless clazz.respond_to?(:loaded?) && clazz.loaded?
|
49
|
+
raise ArgumentError, "Plugin Entitlements::Plugins::#{plugin['class']} should inherit Entitlements::Plugins"
|
50
|
+
end
|
51
|
+
|
52
|
+
unless clazz.respond_to?(:override_hash)
|
53
|
+
raise ArgumentError, "Plugin Entitlements::Plugins::#{plugin['class']} must implement override_hash method"
|
54
|
+
end
|
55
|
+
|
56
|
+
override_hash = clazz.override_hash(group, plugin, ldap)
|
57
|
+
return override_hash if override_hash.is_a?(Hash)
|
58
|
+
|
59
|
+
type = override_hash.class
|
60
|
+
raise ArgumentError, "Plugin Entitlements::Plugins::#{plugin['class']}.override_hash must return hash, not #{type}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|