entitlements-app 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|