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,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Supports predictive entitlements updates. This looks for the believed-to-be-true current membership of groups
4
+ # in a directory of flat files, so as to speed things up by not calling slower APIs. If the calculated membership
5
+ # is not equal to the flat files, then the membership should be recomputed by contacting the API.
6
+ #
7
+ # Reading in cached groups from the supplied directory is global across the entire entitlements system,
8
+ # so this is a singleton class.
9
+
10
+ module Entitlements
11
+ class Data
12
+ class Groups
13
+ class Cached
14
+ include ::Contracts::Core
15
+ C = ::Contracts
16
+
17
+ # Load the caches - read files from dir and populate
18
+ # Entitlements.cache[:predictive_state] for later use. This should only be done once per run.
19
+ #
20
+ # dir - Directory containing the cache.
21
+ #
22
+ # Returns nothing.
23
+ Contract String => nil
24
+ def self.load_caches(dir)
25
+ unless File.directory?(dir)
26
+ raise Errno::ENOENT, "Predictive state directory #{dir.inspect} does not exist!"
27
+ end
28
+
29
+ Entitlements.logger.debug "Loading predictive update caches from #{dir}"
30
+
31
+ Entitlements.cache[:predictive_state] = { by_ou: {}, by_dn: {}, invalid: Set.new }
32
+
33
+ Dir.glob(File.join(dir, "*")).each do |filename|
34
+ dn = File.basename(filename)
35
+ identifier, ou = dn.split(",", 2)
36
+
37
+ file_lines = File.readlines(filename).map(&:strip).map(&:downcase).compact
38
+
39
+ members = file_lines.dup
40
+ members.reject! { |line| line.start_with?("#") }
41
+ members.reject! { |line| line.start_with?("metadata_") }
42
+ member_set = Set.new(members)
43
+
44
+ metadata = file_lines.dup
45
+ unless metadata.empty?
46
+ metadata.select! { |line| line.start_with?("metadata_") }
47
+ metadata = metadata.map { |metadata_string| metadata_string.split "=" }.to_h
48
+ metadata.transform_keys! { |key| key.delete_prefix("metadata_") }
49
+ end
50
+
51
+ Entitlements.cache[:predictive_state][:by_ou][ou] ||= {}
52
+ Entitlements.cache[:predictive_state][:by_ou][ou][identifier] = member_set
53
+ Entitlements.cache[:predictive_state][:by_dn][dn] = { members: member_set, metadata: metadata }
54
+ end
55
+
56
+ Entitlements.logger.debug "Loaded #{Entitlements.cache[:predictive_state][:by_ou].keys.size} OU(s) from cache"
57
+ Entitlements.logger.debug "Loaded #{Entitlements.cache[:predictive_state][:by_dn].keys.size} DN(s) from cache"
58
+
59
+ nil
60
+ end
61
+
62
+ # Invalidate a particular cache entry.
63
+ #
64
+ # dn - A String with the cache entry to invalidate.
65
+ #
66
+ # Returns nothing.
67
+ Contract String => nil
68
+ def self.invalidate(dn)
69
+ return unless Entitlements.cache[:predictive_state]
70
+ Entitlements.cache[:predictive_state][:invalid].add(dn)
71
+ nil
72
+ end
73
+
74
+ # Get a list of members from a particular cached entry.
75
+ #
76
+ # dn - A String with the distinguished name
77
+ #
78
+ # Returns an Set of Strings, or nil if the DN is not in the cache or is invalid.
79
+ Contract String => C::Or[C::SetOf[String], nil]
80
+ def self.members(dn)
81
+ return unless Entitlements.cache[:predictive_state]
82
+
83
+ if Entitlements.cache[:predictive_state][:invalid].member?(dn)
84
+ Entitlements.logger.debug "members(#{dn}): DN has been marked invalid in cache"
85
+ return
86
+ end
87
+
88
+ unless Entitlements.cache[:predictive_state][:by_dn].key?(dn)
89
+ Entitlements.logger.debug "members(#{dn}): DN does not exist in cache"
90
+ return
91
+ end
92
+
93
+ Entitlements.cache[:predictive_state][:by_dn][dn][:members]
94
+ end
95
+
96
+ # Get the metadata from a particular cached entry.
97
+ #
98
+ # dn - A String with the distinguished name
99
+ #
100
+ # Returns a Hash of metadata, or nil if the DN is not in the cache, is invalid, or if there is no metadata
101
+ Contract String => C::Or[C::HashOf[String => C::Any], nil]
102
+ def self.metadata(dn)
103
+ return unless Entitlements.cache[:predictive_state]
104
+
105
+ if Entitlements.cache[:predictive_state][:invalid].member?(dn)
106
+ Entitlements.logger.debug "metadata(#{dn}): DN has been marked invalid in cache"
107
+ return
108
+ end
109
+
110
+ unless Entitlements.cache[:predictive_state][:by_dn].key?(dn)
111
+ Entitlements.logger.debug "metadata(#{dn}): DN does not exist in cache"
112
+ return
113
+ end
114
+
115
+ Entitlements.cache[:predictive_state][:by_dn][dn][:metadata]
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,478 @@
1
+ # frozen_string_literal: true
2
+ # Base class to interact with rules stored in some kind of file or directory structure.
3
+
4
+ require_relative "rules/base"
5
+ require_relative "rules/group"
6
+ require_relative "rules/username"
7
+
8
+ require_relative "filters/base"
9
+ require_relative "filters/member_of_group"
10
+
11
+ require_relative "modifiers/expiration"
12
+
13
+ module Entitlements
14
+ class Data
15
+ class Groups
16
+ class Calculated
17
+ class Base
18
+ include ::Contracts::Core
19
+ C = ::Contracts
20
+
21
+ ALIAS_METHODS = {
22
+ "entitlements_group" => "group"
23
+ }
24
+
25
+ MAX_MODIFIER_ITERATIONS = 100
26
+
27
+ MODIFIERS = %w[
28
+ expiration
29
+ ]
30
+
31
+ attr_reader :filename, :filters, :metadata
32
+
33
+ # ---------------------------------------------
34
+ # Interface which all rule sets must implement.
35
+ # ---------------------------------------------
36
+
37
+ # Get the list of the group members found by applying the rule set.
38
+ #
39
+ # Takes no arguments.
40
+ #
41
+ # Returns Set[Entitlements::Models::Person] of all matching members.
42
+ Contract C::None => C::SetOf[Entitlements::Models::Person]
43
+ def members
44
+ # :nocov:
45
+ raise "Must be implemented in child class"
46
+ # :nocov:
47
+ end
48
+
49
+ # Get the description of the group.
50
+ #
51
+ # Takes no arguments.
52
+ #
53
+ # Returns a String.
54
+ Contract C::None => String
55
+ def description
56
+ # :nocov:
57
+ raise "Must be implemented in child class"
58
+ # :nocov:
59
+ end
60
+
61
+ # Stub modifiers. Override in child class if they are supported for a given entitlement type.
62
+ #
63
+ # Takes no arguments.
64
+ #
65
+ # Returns Hash[<String>key => <Object>value]
66
+ # :nocov:
67
+ Contract C::None => C::HashOf[String => C::Any]
68
+ def modifiers
69
+ {}
70
+ end
71
+ # :nocov:
72
+
73
+ # ---------------------------------------------
74
+ # Helper.
75
+ # ---------------------------------------------
76
+
77
+ # Constructor.
78
+ #
79
+ # filename - Filename with the ruleset.
80
+ # options - An optional hash of additional options.
81
+ Contract C::KeywordArgs[
82
+ filename: String,
83
+ config: C::Maybe[C::HashOf[String => C::Any]],
84
+ options: C::Optional[C::HashOf[Symbol => C::Any]]
85
+ ] => C::Any
86
+ def initialize(filename:, config: nil, options: {})
87
+ @filename = filename
88
+ @config = config
89
+ @options = options
90
+ @metadata = initialize_metadata
91
+ @filters = initialize_filters
92
+ end
93
+
94
+ # Log a fatal message to logger and then exit with the same error message.
95
+ #
96
+ # message - String with the message to log and raise.
97
+ #
98
+ # Returns nothing.
99
+ Contract String => C::None
100
+ def fatal_message(message)
101
+ Entitlements.logger.fatal(message)
102
+ raise RuntimeError, message
103
+ end
104
+
105
+ # Members of the group with filters applied.
106
+ #
107
+ # members_in - Optionally a set of Entitlements::Models::Person with the currently calculated member set.
108
+ #
109
+ # Returns Set[Entitlements::Models::Person] of all matching members.
110
+ Contract C::None => C::Or[:calculating, C::SetOf[Entitlements::Models::Person]]
111
+ def filtered_members
112
+ return :calculating if members == :calculating
113
+
114
+ # Start with the set of all members that are calculated. For each filter that is not set
115
+ # to the special value :all, run the filter to remove progressively more members.
116
+ @filtered_members ||= begin
117
+ result = members.dup
118
+ filters.reject { |_, filter_val| filter_val == :all }.each do |filter_name, filter_val|
119
+ filter_cfg = Entitlements::Data::Groups::Calculated.filters_index[filter_name]
120
+ clazz = filter_cfg.fetch(:class)
121
+ obj = clazz.new(filter: filter_val, config: filter_cfg.fetch(:config, {}))
122
+ # If excluded_paths is set, ignore any of those excluded paths
123
+ unless filter_cfg[:config]["excluded_paths"].nil?
124
+ # if the filename is not in any of the excluded paths, filter it
125
+ unless filter_cfg[:config]["excluded_paths"].any? { |excluded_path| filename.include?(excluded_path) }
126
+ result.reject! { |member| obj.filtered?(member) }
127
+ end
128
+ end
129
+
130
+ # if included_paths is set, filter only files at those included paths
131
+ unless filter_cfg[:config]["included_paths"].nil?
132
+ # if the filename is in any of the included paths, filter it
133
+ if filter_cfg[:config]["included_paths"].any? { |included_path| filename.include?(included_path) }
134
+ result.reject! { |member| obj.filtered?(member) }
135
+ end
136
+ end
137
+
138
+ # if neither included_paths nor excluded_paths are set, filter normally
139
+ if filter_cfg[:config]["included_paths"].nil? and filter_cfg[:config]["excluded_paths"].nil?
140
+ result.reject! { |member| obj.filtered?(member) }
141
+ end
142
+ end
143
+ result
144
+ end
145
+ end
146
+
147
+ # Members of the group with modifiers applied.
148
+ #
149
+ # Takes no arguments.
150
+ #
151
+ # Returns Set[Entitlements::Models::Person] of all matching members.
152
+ Contract C::None => C::Or[:calculating, C::SetOf[Entitlements::Models::Person]]
153
+ def modified_members
154
+ return :calculating if members == :calculating
155
+ @modified_members ||= apply_modifiers(members)
156
+ end
157
+
158
+ # Members of the group with modifiers and filters applied (in that order).
159
+ #
160
+ # members_in - Optionally a set of Entitlements::Models::Person with the currently calculated member set.
161
+ #
162
+ # Returns Set[Entitlements::Models::Person] of all matching members.
163
+ Contract C::None => C::Or[:calculating, C::SetOf[Entitlements::Models::Person]]
164
+ def modified_filtered_members
165
+ return :calculating if filtered_members == :calculating
166
+ @modified_filtered_members ||= apply_modifiers(filtered_members)
167
+ end
168
+
169
+ private
170
+
171
+ attr_reader :config, :options
172
+
173
+ # Common method that takes a given list of members and applies the modifiers.
174
+ # Used to calculated `modified_members` and `modified_filtered_members`.
175
+ #
176
+ # member_set - Set of Entitlements::Models::Person
177
+ #
178
+ # Returns a set of Entitlements::Models::Person
179
+ Contract C::SetOf[Entitlements::Models::Person] => C::SetOf[Entitlements::Models::Person]
180
+ def apply_modifiers(member_set)
181
+ result = member_set.dup
182
+
183
+ modifier_objects = modifiers_constant.map do |m|
184
+ if modifiers.key?(m)
185
+ modifier_class = "Entitlements::Data::Groups::Calculated::Modifiers::" + Entitlements::Util::Util.camelize(m)
186
+ clazz = Kernel.const_get(modifier_class)
187
+ clazz.new(rs: self, config: modifiers.fetch(m))
188
+ else
189
+ nil
190
+ end
191
+ end.compact
192
+
193
+ converged = false
194
+ 1.upto(MAX_MODIFIER_ITERATIONS) do
195
+ unless modifier_objects.select { |modifier| modifier.modify(result) }.any?
196
+ converged = true
197
+ break
198
+ end
199
+ end
200
+
201
+ unless converged
202
+ raise "Modifiers for filename=#{filename} failed to converge after #{MAX_MODIFIER_ITERATIONS} iterations"
203
+ end
204
+
205
+ result
206
+ end
207
+
208
+ # Determine if an entry is expired or not. Return true if expired, false if not.
209
+ #
210
+ # expiration - A String (should have YYYY-MM-DD), or nil.
211
+ # context - A String (usually a filename) to provide context if there's an error.
212
+ #
213
+ # Returns true if expired, false if not expired.
214
+ Contract C::Or[nil, String], String => C::Or[nil, C::Bool]
215
+ def expired?(expiration, context)
216
+ return false if expiration.nil? || expiration.strip.empty?
217
+ if expiration =~ /\A(\d{4})-(\d{2})-(\d{2})\z/
218
+ year, month, day = Regexp.last_match(1).to_i, Regexp.last_match(2).to_i, Regexp.last_match(3).to_i
219
+ return Time.utc(year, month, day, 0, 0, 0) <= Time.now.utc
220
+ end
221
+ message = "Invalid expiration date #{expiration.inspect} in #{context} (expected format: YYYY-MM-DD)"
222
+ raise ArgumentError, message
223
+ end
224
+
225
+ # Wrapper method around `members_from_rules` that calls the method and caches the result.
226
+ #
227
+ # rule - A Hash of rules (see "rules" stub below).
228
+ #
229
+ # Returns Set[Entitlements::Models::Person].
230
+ Contract C::HashOf[String => C::Any] => C::Or[:calculating, C::SetOf[Entitlements::Models::Person]]
231
+ def members_from_rules(rule)
232
+ Entitlements.cache[:calculated] ||= {}
233
+ Entitlements.cache[:calculated][rou] ||= {}
234
+
235
+ # Already calculated it? Just return it.
236
+ return Entitlements.cache[:calculated][rou][cn] if Entitlements.cache[:calculated][rou][cn]
237
+
238
+ # Add it to the list of things we are currently calculating so we can detect and report
239
+ # on circular dependencies later.
240
+ Entitlements.cache[:calculated][rou][cn] = :calculating
241
+ Entitlements.cache[:dependencies] ||= []
242
+ Entitlements.cache[:dependencies] << "#{rou}/#{cn}"
243
+
244
+ # Actually calculate it.
245
+ Entitlements.cache[:calculated][rou][cn] = _members_from_rules(rule)
246
+
247
+ # This should be the last item on the dependencies array, so pop it off.
248
+ unless Entitlements.cache[:dependencies].last == "#{rou}/#{cn}"
249
+ # This would be a bug
250
+ # :nocov:
251
+ raise "Error: Unexpected last item in dependencies: expected #{rou}/#{cn} got #{Entitlements.cache[:dependencies].inspect}"
252
+ # :nocov:
253
+ end
254
+ Entitlements.cache[:dependencies].pop
255
+
256
+ # Return the calculated value.
257
+ Entitlements.cache[:calculated][rou][cn]
258
+ end
259
+
260
+ # Apply logic in a hash of rules, where necessary making calls to the appropriate function
261
+ # in our rules toolbox.
262
+ #
263
+ # rule - A Hash of rules (see "rules" stub below).
264
+ #
265
+ # Returns Set[Entitlements::Models::Person].
266
+ Contract C::HashOf[String => C::Any] => C::SetOf[Entitlements::Models::Person]
267
+ def _members_from_rules(rule)
268
+ # Empty rule => error.
269
+ if rule.keys.empty?
270
+ raise "Rule Error: Rule had no keys in #{filename}!"
271
+ end
272
+
273
+ # The rule should only have one { key => Object }.
274
+ unless rule.keys.size == 1
275
+ raise "Rule Error: Rule had multiple keys #{rule.inspect} in #{filename}!"
276
+ end
277
+
278
+ # Always => false is special. It returns nothing.
279
+ if rule["always"] == false
280
+ return Set.new
281
+ end
282
+
283
+ # Go through the rule. Each key is one of the following:
284
+ # - "or": An array of conditions; if any is true, this rule is true
285
+ # - "and": An array of conditions; if all are true, the rule is true
286
+ # - "not": Takes a hash (can also be "and" / "or"); if it's true, the rule is false
287
+ # - anything else: Must correspond to something in "rules"
288
+ function = function_for(rule.first.first)
289
+ obj = rule.first.last
290
+
291
+ return handle_or(obj) if function == "or"
292
+ return handle_and(obj) if function == "and"
293
+ return handle_not(obj) if function == "not"
294
+
295
+ unless allowed_methods.member?(function)
296
+ Entitlements.logger.fatal "The method #{function.inspect} is not permitted in #{filename}!"
297
+ raise "Rule Error: #{function} is not a valid function in #{filename}!"
298
+ end
299
+
300
+ clazz = Entitlements::Data::Groups::Calculated.rules_index[function]
301
+ clazz.matches(value: obj, filename: filename, options: options)
302
+ end
303
+
304
+ # Obtain the rule set from the YAML file and convert it to an object. Cache this the first
305
+ # time it happens, because this code is going to be called once per person!
306
+ #
307
+ # Takes no arguments.
308
+ #
309
+ # Returns a Hash.
310
+ Contract C::None => C::HashOf[String => C::Any]
311
+ def rules
312
+ # :nocov:
313
+ raise "Must be implemented in child class"
314
+ # :nocov:
315
+ end
316
+
317
+ # Handle boolean OR logic.
318
+ # Do not enforce contract here since this is user-provided logic and we want a friendlier message.
319
+ #
320
+ # rule - (Hopefully) Array[Hash[String => obj]]
321
+ #
322
+ # Returns C::SetOf[Entitlements::Models::Person] from a recursive call.
323
+ def handle_or(rule)
324
+ ensure_type!("or", rule, Array)
325
+ result = Set.new
326
+ rule.each do |item|
327
+ ensure_type!("or", item, Hash)
328
+ item_result = _members_from_rules(item)
329
+ result.merge item_result
330
+ end
331
+ result
332
+ end
333
+
334
+ # Handle boolean AND logic.
335
+ # Do not enforce contract here since this is user-provided logic and we want a friendlier message.
336
+ #
337
+ # rule - (Hopefully) Array[Hash[String => obj]]
338
+ #
339
+ # Returns C::SetOf[Entitlements::Models::Person] from a recursive call.
340
+ def handle_and(rule)
341
+ ensure_type!("and", rule, Array)
342
+ return result unless rule.any?
343
+
344
+ first_rule = rule.shift
345
+ ensure_type!("and", first_rule, Hash)
346
+ result = _members_from_rules(first_rule)
347
+
348
+ rule.each do |item|
349
+ ensure_type!("and", item, Hash)
350
+ item_result = _members_from_rules(item)
351
+ result = result & item_result
352
+ end
353
+
354
+ result
355
+ end
356
+
357
+ # Handle boolean NOT logic.
358
+ # Do not enforce contract here since this is user-provided logic and we want a friendlier message.
359
+ #
360
+ # rule - (Hopefully) Hash[String => obj]
361
+ #
362
+ # Returns C::SetOf[Entitlements::Models::Person] from a recursive call.
363
+ def handle_not(rule)
364
+ ensure_type!("not", rule, Hash)
365
+ all_people = Set.new(Entitlements.cache[:people_obj].read.map { |_, obj| obj })
366
+ matches = _members_from_rules(rule)
367
+ all_people - matches
368
+ end
369
+
370
+ # ensure_type!: Force the incoming argument to be the indicated type. Not handled
371
+ # via the contracts mechanism so a friendlier error is printed, since it's
372
+ # user code that would break this.
373
+ #
374
+ # function - A String with the function name (e.g. "or" or some rule)
375
+ # obj - The object that's supposed to be the indicated type
376
+ # type - The type.
377
+ #
378
+ # Returns nothing, but raises an error if the type doesn't match.
379
+ Contract String, C::Any, C::Any => nil
380
+ def ensure_type!(function, obj, type)
381
+ return if obj.is_a?(type)
382
+ raise "Invalid type: in #{filename}, expected #{function.inspect} to be a #{type} but got #{obj.inspect}!"
383
+ end
384
+
385
+ # Convert a string into CamelCase.
386
+ #
387
+ # str - The string that needs to be converted to CamelCase.
388
+ #
389
+ # Returns a String in CamelCase.
390
+ Contract String => String
391
+ def camelize(str)
392
+ Entitlements::Util::Util.camelize(str)
393
+ end
394
+
395
+ # Determine the ou from the filename (it's the last directory name).
396
+ #
397
+ # Takes no arguments.
398
+ #
399
+ # Returns a String with the name of the ou.
400
+ Contract C::None => String
401
+ def ou
402
+ File.basename(File.dirname(filename))
403
+ end
404
+
405
+ # Determine the relatiive ou from the filename (relative to the config root)
406
+ #
407
+ # Takes no arguments.
408
+ #
409
+ # Returns a String with the name of the ou.
410
+ Contract C::None => String
411
+ def rou
412
+ File.expand_path(File.dirname(filename)).gsub("#{Entitlements.config_path}/", "").gsub(/^\//, "").gsub(/\//, "/")
413
+ end
414
+
415
+ # Determine the cn from the filename (it's the filename without the extension).
416
+ #
417
+ # Takes no arguments.
418
+ #
419
+ # Returns a String with the name of the cn.
420
+ Contract C::None => String
421
+ def cn
422
+ File.basename(filename).sub(/\.[^\.]+\z/, "")
423
+ end
424
+
425
+ # Get the permitted methods for this ou. Defaults to whitelisted methods from base class which
426
+ # allows any supported method, but can be locked down further by setting `allowed_methods`
427
+ # in the configuration for the ou or overriding whitelisted methods for the class.
428
+ #
429
+ # Takes no arguments.
430
+ #
431
+ # Returns a Set with the permitted methods.
432
+ Contract C::None => C::SetOf[String]
433
+ def allowed_methods
434
+ @allowed_methods ||= begin
435
+ if config.is_a?(Hash) && config["allowed_methods"]
436
+ unless config["allowed_methods"].is_a?(Array)
437
+ raise ArgumentError, "allowed_methods must be an Array in #{filename}!"
438
+ end
439
+ Set.new(whitelisted_methods.to_a & config["allowed_methods"])
440
+ else
441
+ whitelisted_methods
442
+ end
443
+ end
444
+ end
445
+
446
+ # Get the method for a given function. Returns the underlying function if the entry is an
447
+ # alias, or else returns what was entered. The caller is responsible to validate that the
448
+ # function is valid and whitelisted.
449
+ #
450
+ # function_in - String with the function name from the definition.
451
+ #
452
+ # Returns the underlying function name if aliased, or else what was entered.
453
+ Contract String => String
454
+ def function_for(function_in)
455
+ ALIAS_METHODS[function_in] || function_in
456
+ end
457
+
458
+ # Get the whitelisted methods. This is just set to the constant in the base class, but
459
+ # is defined as a method so it can be overridden in the child class if needed.
460
+ #
461
+ # Takes no arguments.
462
+ #
463
+ # Returns an Set of Strings with allowed methods.
464
+ Contract C::None => C::SetOf[String]
465
+ def whitelisted_methods
466
+ Set.new(Entitlements::Data::Groups::Calculated.rules_index.keys)
467
+ end
468
+
469
+ # Get the value of the modifiers constant. This is here so it can be stubbed
470
+ # in CI testing more easily.
471
+ def modifiers_constant
472
+ MODIFIERS
473
+ end
474
+ end
475
+ end
476
+ end
477
+ end
478
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Filter class to remove members of a particular LDAP group.
4
+
5
+ require "yaml"
6
+
7
+ module Entitlements
8
+ class Data
9
+ class Groups
10
+ class Calculated
11
+ class Filters
12
+ class Base
13
+ include ::Contracts::Core
14
+ C = ::Contracts
15
+
16
+ # Interface method: Determine if the member is filtered as per this definition.
17
+ #
18
+ # member - Entitlements::Models::Person object
19
+ #
20
+ # Return true if the member is to be filtered out, false if the member does not match the filter.
21
+ Contract Entitlements::Models::Person => C::Bool
22
+ def filtered?(_member)
23
+ # :nocov:
24
+ raise "Must be implemented in child class"
25
+ # :nocov:
26
+ end
27
+
28
+ # Constructor.
29
+ #
30
+ # filter - Either :none, :all, or an array of string conditions passed through to the filter
31
+ # config - Configuration data (Hash, optional)
32
+ Contract C::KeywordArgs[
33
+ filter: C::Or[:none, C::ArrayOf[String]],
34
+ config: C::Maybe[Hash]
35
+ ] => C::Any
36
+ def initialize(filter:, config: {})
37
+ @filter = filter
38
+ @config = config
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :config, :filter
44
+
45
+ # Helper method: Determine if the person is listed in an array of filter conditions.
46
+ # Filter conditions that have no `/` are interpreted to be usernames, whereas filter
47
+ # conditions with a `/` refer to LDAP entitlement groups.
48
+ #
49
+ # member - Entitlements::Models::Person object
50
+ #
51
+ # Returns true if a member of the filter conditions, false otherwise.
52
+ Contract Entitlements::Models::Person => C::Bool
53
+ def member_of_filter?(member)
54
+ # First handle all username entries, regardless of order, because we do not
55
+ # have to mess around with reading groups for those.
56
+ filter.reject { |filter_val| filter_val =~ /\// }.each do |filter_val|
57
+ return true if filter_val.downcase == member.uid.downcase
58
+ end
59
+
60
+ # Now handle all group entries.
61
+ filter.select { |filter_val| filter_val =~ /\// }.each do |filter_val|
62
+ return true if member_of_named_group?(member, filter_val)
63
+ end
64
+
65
+ # If we get here there was no match.
66
+ false
67
+ end
68
+
69
+ # Helper method: Determine if the person is a member of a specific LDAP group named
70
+ # in entitlements style.
71
+ #
72
+ # member - Entitlements::Models::Person object
73
+ # group_ref - Optionally a string with a reference to a group to look up
74
+ #
75
+ # Returns true if a member of the group, false otherwise.
76
+ Contract Entitlements::Models::Person, String => C::Bool
77
+ def member_of_named_group?(member, group_ref)
78
+ Entitlements.cache[:member_of_named_group] ||= {}
79
+ Entitlements.cache[:member_of_named_group][group_ref] ||= begin
80
+ member_set = Entitlements::Data::Groups::Calculated::Rules::Group.matches(
81
+ value: group_ref,
82
+ )
83
+ member_set.map { |person| person.uid.downcase }
84
+ end
85
+
86
+ Entitlements.cache[:member_of_named_group][group_ref].include?(member.uid.downcase)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end