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.
- checksums.yaml +7 -0
- data/VERSION +1 -0
- data/bin/deploy-entitlements +18 -0
- data/lib/entitlements/auditor/base.rb +163 -0
- data/lib/entitlements/backend/base_controller.rb +171 -0
- data/lib/entitlements/backend/base_provider.rb +55 -0
- data/lib/entitlements/backend/dummy/controller.rb +89 -0
- data/lib/entitlements/backend/dummy.rb +3 -0
- data/lib/entitlements/backend/ldap/controller.rb +188 -0
- data/lib/entitlements/backend/ldap/provider.rb +128 -0
- data/lib/entitlements/backend/ldap.rb +4 -0
- data/lib/entitlements/backend/member_of/controller.rb +203 -0
- data/lib/entitlements/backend/member_of.rb +3 -0
- data/lib/entitlements/cli.rb +121 -0
- data/lib/entitlements/data/groups/cached.rb +120 -0
- data/lib/entitlements/data/groups/calculated/base.rb +478 -0
- data/lib/entitlements/data/groups/calculated/filters/base.rb +93 -0
- data/lib/entitlements/data/groups/calculated/filters/member_of_group.rb +32 -0
- data/lib/entitlements/data/groups/calculated/modifiers/base.rb +38 -0
- data/lib/entitlements/data/groups/calculated/modifiers/expiration.rb +56 -0
- data/lib/entitlements/data/groups/calculated/ruby.rb +137 -0
- data/lib/entitlements/data/groups/calculated/rules/base.rb +35 -0
- data/lib/entitlements/data/groups/calculated/rules/group.rb +129 -0
- data/lib/entitlements/data/groups/calculated/rules/username.rb +41 -0
- data/lib/entitlements/data/groups/calculated/text.rb +337 -0
- data/lib/entitlements/data/groups/calculated/yaml.rb +171 -0
- data/lib/entitlements/data/groups/calculated.rb +290 -0
- data/lib/entitlements/data/groups.rb +13 -0
- data/lib/entitlements/data/people/combined.rb +197 -0
- data/lib/entitlements/data/people/dummy.rb +71 -0
- data/lib/entitlements/data/people/ldap.rb +142 -0
- data/lib/entitlements/data/people/yaml.rb +102 -0
- data/lib/entitlements/data/people.rb +58 -0
- data/lib/entitlements/extras/base.rb +40 -0
- data/lib/entitlements/extras/ldap_group/base.rb +20 -0
- data/lib/entitlements/extras/ldap_group/filters/member_of_ldap_group.rb +50 -0
- data/lib/entitlements/extras/ldap_group/rules/ldap_group.rb +69 -0
- data/lib/entitlements/extras/orgchart/base.rb +32 -0
- data/lib/entitlements/extras/orgchart/logic.rb +171 -0
- data/lib/entitlements/extras/orgchart/person_methods.rb +55 -0
- data/lib/entitlements/extras/orgchart/rules/direct_report.rb +62 -0
- data/lib/entitlements/extras/orgchart/rules/management.rb +59 -0
- data/lib/entitlements/extras.rb +82 -0
- data/lib/entitlements/models/action.rb +82 -0
- data/lib/entitlements/models/group.rb +280 -0
- data/lib/entitlements/models/person.rb +149 -0
- data/lib/entitlements/plugins/dummy.rb +22 -0
- data/lib/entitlements/plugins/group_of_names.rb +28 -0
- data/lib/entitlements/plugins/posix_group.rb +46 -0
- data/lib/entitlements/plugins.rb +13 -0
- data/lib/entitlements/rule/base.rb +74 -0
- data/lib/entitlements/service/ldap.rb +405 -0
- data/lib/entitlements/util/mirror.rb +42 -0
- data/lib/entitlements/util/override.rb +64 -0
- data/lib/entitlements/util/util.rb +219 -0
- data/lib/entitlements.rb +606 -0
- metadata +343 -0
@@ -0,0 +1,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
|