entitlements 0.1.7

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.
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