entitlements-app 0.1.6

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