entitlements 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/VERSION +1 -0
  3. data/bin/deploy-entitlements +18 -0
  4. data/lib/entitlements/auditor/base.rb +163 -0
  5. data/lib/entitlements/backend/base_controller.rb +171 -0
  6. data/lib/entitlements/backend/base_provider.rb +55 -0
  7. data/lib/entitlements/backend/dummy/controller.rb +89 -0
  8. data/lib/entitlements/backend/dummy.rb +3 -0
  9. data/lib/entitlements/backend/ldap/controller.rb +188 -0
  10. data/lib/entitlements/backend/ldap/provider.rb +128 -0
  11. data/lib/entitlements/backend/ldap.rb +4 -0
  12. data/lib/entitlements/backend/member_of/controller.rb +203 -0
  13. data/lib/entitlements/backend/member_of.rb +3 -0
  14. data/lib/entitlements/cli.rb +121 -0
  15. data/lib/entitlements/data/groups/cached.rb +120 -0
  16. data/lib/entitlements/data/groups/calculated/base.rb +478 -0
  17. data/lib/entitlements/data/groups/calculated/filters/base.rb +93 -0
  18. data/lib/entitlements/data/groups/calculated/filters/member_of_group.rb +32 -0
  19. data/lib/entitlements/data/groups/calculated/modifiers/base.rb +38 -0
  20. data/lib/entitlements/data/groups/calculated/modifiers/expiration.rb +56 -0
  21. data/lib/entitlements/data/groups/calculated/ruby.rb +137 -0
  22. data/lib/entitlements/data/groups/calculated/rules/base.rb +35 -0
  23. data/lib/entitlements/data/groups/calculated/rules/group.rb +129 -0
  24. data/lib/entitlements/data/groups/calculated/rules/username.rb +41 -0
  25. data/lib/entitlements/data/groups/calculated/text.rb +337 -0
  26. data/lib/entitlements/data/groups/calculated/yaml.rb +171 -0
  27. data/lib/entitlements/data/groups/calculated.rb +290 -0
  28. data/lib/entitlements/data/groups.rb +13 -0
  29. data/lib/entitlements/data/people/combined.rb +197 -0
  30. data/lib/entitlements/data/people/dummy.rb +71 -0
  31. data/lib/entitlements/data/people/ldap.rb +142 -0
  32. data/lib/entitlements/data/people/yaml.rb +102 -0
  33. data/lib/entitlements/data/people.rb +58 -0
  34. data/lib/entitlements/extras/base.rb +40 -0
  35. data/lib/entitlements/extras/ldap_group/base.rb +20 -0
  36. data/lib/entitlements/extras/ldap_group/filters/member_of_ldap_group.rb +50 -0
  37. data/lib/entitlements/extras/ldap_group/rules/ldap_group.rb +69 -0
  38. data/lib/entitlements/extras/orgchart/base.rb +32 -0
  39. data/lib/entitlements/extras/orgchart/logic.rb +171 -0
  40. data/lib/entitlements/extras/orgchart/person_methods.rb +55 -0
  41. data/lib/entitlements/extras/orgchart/rules/direct_report.rb +62 -0
  42. data/lib/entitlements/extras/orgchart/rules/management.rb +59 -0
  43. data/lib/entitlements/extras.rb +82 -0
  44. data/lib/entitlements/models/action.rb +82 -0
  45. data/lib/entitlements/models/group.rb +280 -0
  46. data/lib/entitlements/models/person.rb +149 -0
  47. data/lib/entitlements/plugins/dummy.rb +22 -0
  48. data/lib/entitlements/plugins/group_of_names.rb +28 -0
  49. data/lib/entitlements/plugins/posix_group.rb +46 -0
  50. data/lib/entitlements/plugins.rb +13 -0
  51. data/lib/entitlements/rule/base.rb +74 -0
  52. data/lib/entitlements/service/ldap.rb +405 -0
  53. data/lib/entitlements/util/mirror.rb +42 -0
  54. data/lib/entitlements/util/override.rb +64 -0
  55. data/lib/entitlements/util/util.rb +219 -0
  56. data/lib/entitlements.rb +606 -0
  57. 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