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,290 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "calculated/base"
|
4
|
+
require_relative "calculated/ruby"
|
5
|
+
require_relative "calculated/text"
|
6
|
+
require_relative "calculated/yaml"
|
7
|
+
|
8
|
+
# Calculate groups that should exist and the contents of each based on a set of rules
|
9
|
+
# defined within a directory. The calculation of members is global across the entire
|
10
|
+
# entitlements system, so this is a singleton class.
|
11
|
+
|
12
|
+
module Entitlements
|
13
|
+
class Data
|
14
|
+
class Groups
|
15
|
+
class Calculated
|
16
|
+
include ::Contracts::Core
|
17
|
+
C = ::Contracts
|
18
|
+
|
19
|
+
FILE_EXTENSIONS = {
|
20
|
+
"rb" => "Entitlements::Data::Groups::Calculated::Ruby",
|
21
|
+
"txt" => "Entitlements::Data::Groups::Calculated::Text",
|
22
|
+
"yaml" => "Entitlements::Data::Groups::Calculated::YAML"
|
23
|
+
}
|
24
|
+
|
25
|
+
@groups_in_ou_cache = {}
|
26
|
+
@groups_cache = {}
|
27
|
+
@config_cache = {}
|
28
|
+
|
29
|
+
# Reset all module state
|
30
|
+
#
|
31
|
+
# Takes no arguments
|
32
|
+
def self.reset!
|
33
|
+
@rules_index = {
|
34
|
+
"group" => Entitlements::Data::Groups::Calculated::Rules::Group,
|
35
|
+
"username" => Entitlements::Data::Groups::Calculated::Rules::Username
|
36
|
+
}
|
37
|
+
|
38
|
+
@filters_index = {}
|
39
|
+
@groups_in_ou_cache = {}
|
40
|
+
@groups_cache = {}
|
41
|
+
@config_cache = {}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Construct a group object.
|
45
|
+
#
|
46
|
+
# Takes no arguments.
|
47
|
+
#
|
48
|
+
# Returns a Entitlements::Models::Group object.
|
49
|
+
Contract String => Entitlements::Models::Group
|
50
|
+
def self.read(dn)
|
51
|
+
return @groups_cache[dn] if @groups_cache[dn]
|
52
|
+
raise "read(#{dn.inspect}) does not support calculation at this time. Please use read_all() first to build cache."
|
53
|
+
end
|
54
|
+
|
55
|
+
# Calculate all groups within the specified OU and enumerate their members.
|
56
|
+
# Results are cached for future runs so the read() method is faster.
|
57
|
+
#
|
58
|
+
# ou_key - String with the key from the configuration file.
|
59
|
+
# cfg_obj - Hash with the configuration for that key from the configuration file.
|
60
|
+
#
|
61
|
+
# Returns a Set of Strings (DNs) of the groups in this OU.
|
62
|
+
Contract String, C::HashOf[String => C::Any], C::KeywordArgs[
|
63
|
+
skip_broken_references: C::Optional[C::Bool]
|
64
|
+
] => C::SetOf[String]
|
65
|
+
def self.read_all(ou_key, cfg_obj, skip_broken_references: false)
|
66
|
+
return read_mirror(ou_key, cfg_obj) if cfg_obj["mirror"]
|
67
|
+
|
68
|
+
@config_cache[ou_key] ||= cfg_obj
|
69
|
+
@groups_in_ou_cache[ou_key] ||= begin
|
70
|
+
Entitlements.logger.debug "Calculating all groups for #{ou_key}"
|
71
|
+
Entitlements.logger.debug "!!! skip_broken_references is enabled" if skip_broken_references
|
72
|
+
|
73
|
+
result = Set.new
|
74
|
+
Entitlements.cache[:file_objects] ||= {}
|
75
|
+
|
76
|
+
# Iterate over all the files in the configuration directory for this OU
|
77
|
+
path = Entitlements::Util::Util.path_for_group(ou_key)
|
78
|
+
Dir.glob(File.join(path, "*")).each do |filename|
|
79
|
+
# If it's a directory, skip it for now.
|
80
|
+
if File.directory?(filename)
|
81
|
+
next
|
82
|
+
end
|
83
|
+
|
84
|
+
# If the file is ignored (e.g. documentation) then skip it.
|
85
|
+
if Entitlements::IGNORED_FILES.member?(File.basename(filename))
|
86
|
+
next
|
87
|
+
end
|
88
|
+
|
89
|
+
# Determine the group DN. The CN will be the filname without its extension.
|
90
|
+
file_without_extension = File.basename(filename).sub(/\.\w+\z/, "")
|
91
|
+
unless file_without_extension =~ /\A[\w\-]+\z/
|
92
|
+
raise "Illegal LDAP group name #{file_without_extension.inspect} in #{ou_key}!"
|
93
|
+
end
|
94
|
+
group_dn = ["cn=#{file_without_extension}", cfg_obj.fetch("base")].join(",")
|
95
|
+
|
96
|
+
# Use the ruleset to build the group.
|
97
|
+
options = { skip_broken_references: skip_broken_references }
|
98
|
+
|
99
|
+
Entitlements.cache[:file_objects][filename] ||= ruleset(filename: filename, config: cfg_obj, options: options)
|
100
|
+
@groups_cache[group_dn] = Entitlements::Models::Group.new(
|
101
|
+
dn: group_dn,
|
102
|
+
members: Entitlements.cache[:file_objects][filename].modified_filtered_members,
|
103
|
+
description: Entitlements.cache[:file_objects][filename].description,
|
104
|
+
metadata: Entitlements.cache[:file_objects][filename].metadata.merge("_filename" => filename)
|
105
|
+
)
|
106
|
+
result.add group_dn
|
107
|
+
end
|
108
|
+
|
109
|
+
result
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Return the group cache as a hash.
|
114
|
+
#
|
115
|
+
# Takes no arguments.
|
116
|
+
#
|
117
|
+
# Returns a hash { dn => Entitlements::Models::Group }
|
118
|
+
# :nocov:
|
119
|
+
Contract C::None => C::HashOf[String => Entitlements::Models::Group]
|
120
|
+
def self.to_h
|
121
|
+
@groups_cache
|
122
|
+
end
|
123
|
+
# :nocov:
|
124
|
+
|
125
|
+
# Get the entire output organized by OU.
|
126
|
+
#
|
127
|
+
# Takes no arguments.
|
128
|
+
#
|
129
|
+
# Returns a Hash of OU to the configuration and group objects it contains.
|
130
|
+
Contract C::None => C::HashOf[String => { config: C::HashOf[String => C::Any], groups: C::HashOf[String => Entitlements::Models::Group]}]
|
131
|
+
def self.all_groups
|
132
|
+
@groups_in_ou_cache.map do |ou_key, dn_in_ou|
|
133
|
+
if @config_cache.key?(ou_key)
|
134
|
+
[
|
135
|
+
ou_key,
|
136
|
+
{
|
137
|
+
config: @config_cache[ou_key],
|
138
|
+
groups: dn_in_ou.sort.map { |dn| [dn, @groups_cache.fetch(dn)] }.to_h
|
139
|
+
}
|
140
|
+
]
|
141
|
+
else
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
end.compact.to_h
|
145
|
+
end
|
146
|
+
|
147
|
+
# Calculate the groups within the specified mirrored OU. This requires that
|
148
|
+
# read_all() has run already on the mirrored OU. This will not re-calculate
|
149
|
+
# the results, but rather just duplicate the results and adjust the OUs.
|
150
|
+
#
|
151
|
+
# ou_key - String with the key from the configuration file.
|
152
|
+
# cfg_obj - Hash with the configuration for that key from the configuration file.
|
153
|
+
#
|
154
|
+
# Returns a Set of Strings (DNs) of the groups in this OU.
|
155
|
+
Contract String, C::HashOf[String => C::Any] => C::SetOf[String]
|
156
|
+
def self.read_mirror(ou_key, cfg_obj)
|
157
|
+
@groups_in_ou_cache[ou_key] ||= begin
|
158
|
+
Entitlements.logger.debug "Mirroring #{ou_key} from #{cfg_obj['mirror']}"
|
159
|
+
|
160
|
+
unless @groups_in_ou_cache[cfg_obj["mirror"]]
|
161
|
+
raise "Cannot read_mirror on #{ou_key.inspect} because read_all has not occurred on #{cfg_obj['mirror'].inspect}!"
|
162
|
+
end
|
163
|
+
|
164
|
+
result = Set.new
|
165
|
+
@groups_in_ou_cache[cfg_obj["mirror"]].each do |source_dn|
|
166
|
+
source_group = @groups_cache[source_dn]
|
167
|
+
unless source_group
|
168
|
+
raise "No group has been calculated for #{source_dn.inspect}!"
|
169
|
+
end
|
170
|
+
|
171
|
+
new_dn = ["cn=#{source_group.cn}", cfg_obj["base"]].join(",")
|
172
|
+
@groups_cache[new_dn] ||= source_group.copy_of(new_dn)
|
173
|
+
result.add new_dn
|
174
|
+
end
|
175
|
+
|
176
|
+
result
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Construct the ruleset object for a given filename.
|
181
|
+
#
|
182
|
+
# filename - A String with the filename.
|
183
|
+
#
|
184
|
+
# Returns an Entitlements::Data::Groups::Calculated::* object.
|
185
|
+
Contract C::KeywordArgs[
|
186
|
+
filename: String,
|
187
|
+
config: C::HashOf[String => C::Any],
|
188
|
+
options: C::Optional[C::HashOf[Symbol => C::Any]]
|
189
|
+
] => C::Or[
|
190
|
+
Entitlements::Data::Groups::Calculated::Ruby,
|
191
|
+
Entitlements::Data::Groups::Calculated::Text,
|
192
|
+
Entitlements::Data::Groups::Calculated::YAML,
|
193
|
+
]
|
194
|
+
def self.ruleset(filename:, config:, options: {})
|
195
|
+
unless filename =~ /\.(\w+)\z/
|
196
|
+
raise ArgumentError, "Unable to determine the extension on #{filename.inspect}!"
|
197
|
+
end
|
198
|
+
ext = Regexp.last_match(1)
|
199
|
+
|
200
|
+
unless FILE_EXTENSIONS[ext]
|
201
|
+
Entitlements.logger.fatal "Unable to map filename #{filename.inspect} to a ruleset object!"
|
202
|
+
raise ArgumentError, "Unable to map filename #{filename.inspect} to a ruleset object!"
|
203
|
+
end
|
204
|
+
|
205
|
+
if config.key?("allowed_types")
|
206
|
+
unless config["allowed_types"].is_a?(Array)
|
207
|
+
Entitlements.logger.fatal "Configuration error: allowed_types should be an Array, got #{config['allowed_types'].inspect}"
|
208
|
+
raise ArgumentError, "Configuration error: allowed_types should be an Array, got #{config['allowed_types'].class}!"
|
209
|
+
end
|
210
|
+
|
211
|
+
unless config["allowed_types"].include?(ext)
|
212
|
+
allowed_join = config["allowed_types"].join(",")
|
213
|
+
Entitlements.logger.fatal "Files with extension #{ext.inspect} are not allowed in this OU! Allowed: #{allowed_join}!"
|
214
|
+
raise ArgumentError, "Files with extension #{ext.inspect} are not allowed in this OU! Allowed: #{allowed_join}!"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
clazz = Kernel.const_get(FILE_EXTENSIONS[ext])
|
219
|
+
clazz.new(filename: filename, config: config, options: options)
|
220
|
+
end
|
221
|
+
|
222
|
+
#########################
|
223
|
+
# This section is handled as a class variable not an instance variable because rule definitions
|
224
|
+
# are global throughout the program.
|
225
|
+
#########################
|
226
|
+
|
227
|
+
@rules_index = {
|
228
|
+
"group" => Entitlements::Data::Groups::Calculated::Rules::Group,
|
229
|
+
"username" => Entitlements::Data::Groups::Calculated::Rules::Username
|
230
|
+
}
|
231
|
+
|
232
|
+
@filters_index = {}
|
233
|
+
|
234
|
+
# Retrieve the current rules index from the class.
|
235
|
+
#
|
236
|
+
# Takes no arguments.
|
237
|
+
#
|
238
|
+
# Returns a Hash.
|
239
|
+
Contract C::None => C::HashOf[String => Class]
|
240
|
+
def self.rules_index
|
241
|
+
@rules_index
|
242
|
+
end
|
243
|
+
|
244
|
+
# Retrieve the current filters index from the class.
|
245
|
+
#
|
246
|
+
# Takes no arguments.
|
247
|
+
#
|
248
|
+
# Returns a Hash.
|
249
|
+
Contract C::None => C::HashOf[String => Object]
|
250
|
+
def self.filters_index
|
251
|
+
@filters_index
|
252
|
+
end
|
253
|
+
|
254
|
+
# Retrieve the filters in the format used as default / starting point in other parts
|
255
|
+
# of the program: { "filter_name_1" => :none, "filter_name_2" => :none }
|
256
|
+
#
|
257
|
+
# Takes no arguments.
|
258
|
+
#
|
259
|
+
# Returns a Hash in the indicated format.
|
260
|
+
def self.filters_default
|
261
|
+
@filters_index.map { |k, _| [k, :none] }.to_h
|
262
|
+
end
|
263
|
+
|
264
|
+
# Register a rule (requires namespace and class references). Methods are registered
|
265
|
+
# per rule, not per instantiation.
|
266
|
+
#
|
267
|
+
# rule_name - String with the rule name.
|
268
|
+
# clazz - Class that implements the rule.
|
269
|
+
#
|
270
|
+
# Returns the class that implements the rule.
|
271
|
+
Contract String, Class => Class
|
272
|
+
def self.register_rule(rule_name, clazz)
|
273
|
+
@rules_index[rule_name] = clazz
|
274
|
+
end
|
275
|
+
|
276
|
+
# Register a filter (requires namespace and object references). Named filters are instantiated
|
277
|
+
# objects. It's possible to have multiple instantiations of the same class of filter.
|
278
|
+
#
|
279
|
+
# filter_name - String with the filter name.
|
280
|
+
# filter_cfg - Hash with a configuration for the filter.
|
281
|
+
#
|
282
|
+
# Returns the configuration of the filter object (a hash).
|
283
|
+
Contract String, C::HashOf[Symbol => Object] => C::HashOf[Symbol => Object]
|
284
|
+
def self.register_filter(filter_name, filter_cfg)
|
285
|
+
@filters_index[filter_name] = filter_cfg
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "groups/cached"
|
4
|
+
require_relative "groups/calculated"
|
5
|
+
|
6
|
+
module Entitlements
|
7
|
+
class Data
|
8
|
+
class Groups
|
9
|
+
class DuplicateGroupError < RuntimeError; end
|
10
|
+
class GroupNotFoundError < RuntimeError; end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Entitlements
|
6
|
+
class Data
|
7
|
+
class People
|
8
|
+
class Combined
|
9
|
+
include ::Contracts::Core
|
10
|
+
C = ::Contracts
|
11
|
+
|
12
|
+
PARAMETERS = {
|
13
|
+
"operator" => { required: true, type: String },
|
14
|
+
"components" => { required: true, type: Array },
|
15
|
+
}
|
16
|
+
|
17
|
+
# Fingerprint for the object based on unique parameters from the group configuration. If the fingerprint
|
18
|
+
# matches the same object should be re-used. This will raise an error if insufficient configuration is
|
19
|
+
# given.
|
20
|
+
#
|
21
|
+
# config - Hash of configuration values as may be found in the Entitlements configuration file.
|
22
|
+
#
|
23
|
+
# Returns a String with the "fingerprint" for this configuration.
|
24
|
+
Contract C::HashOf[String => C::Any] => String
|
25
|
+
def self.fingerprint(config)
|
26
|
+
# Fingerprint of the combined provider is the fingerprint of each constitutent provider. Then serialize
|
27
|
+
# to JSON to account for the and/or operator that is part of this configuration. Note: this method might
|
28
|
+
# end up being called recursively depending on the complexity of the combined configuration.
|
29
|
+
fingerprints = config["components"].map do |component|
|
30
|
+
Entitlements::Data::People.class_for_config(component).fingerprint(component.fetch("config"))
|
31
|
+
end
|
32
|
+
|
33
|
+
JSON.generate(config.fetch("operator") => fingerprints)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Construct this object based on parameters in a group configuration. This is the direct translation
|
37
|
+
# between the Entitlements configuration file (which is always a Hash with configuration values) and
|
38
|
+
# the object constructed from this class (which can have whatever structure makes sense).
|
39
|
+
#
|
40
|
+
# config - Hash of configuration values as may be found in the Entitlements configuration file.
|
41
|
+
#
|
42
|
+
# Returns Entitlements::Data::People::Combined object.
|
43
|
+
# :nocov:
|
44
|
+
Contract C::HashOf[String => C::Any] => Entitlements::Data::People::Combined
|
45
|
+
def self.new_from_config(config)
|
46
|
+
new(
|
47
|
+
operator: config.fetch("operator"),
|
48
|
+
components: config.fetch("components")
|
49
|
+
)
|
50
|
+
end
|
51
|
+
# :nocov:
|
52
|
+
|
53
|
+
# Validate configuration options.
|
54
|
+
#
|
55
|
+
# key - String with the name of the data source.
|
56
|
+
# config - Hash with the configuration data.
|
57
|
+
#
|
58
|
+
# Returns nothing.
|
59
|
+
Contract String, C::HashOf[String => C::Any] => nil
|
60
|
+
def self.validate_config!(key, config)
|
61
|
+
text = "Combined people configuration for data source #{key.inspect}"
|
62
|
+
Entitlements::Util::Util.validate_attr!(PARAMETERS, config, text)
|
63
|
+
|
64
|
+
unless %w[and or].include?(config["operator"])
|
65
|
+
raise ArgumentError, "In #{key}, expected 'operator' to be either 'and' or 'or', not #{config['operator'].inspect}!"
|
66
|
+
end
|
67
|
+
|
68
|
+
component_spec = {
|
69
|
+
"config" => { required: true, type: Hash },
|
70
|
+
"name" => { required: false, type: String },
|
71
|
+
"type" => { required: true, type: String },
|
72
|
+
}
|
73
|
+
|
74
|
+
if config["components"].empty?
|
75
|
+
raise ArgumentError, "In #{key}, the array of components cannot be empty!"
|
76
|
+
end
|
77
|
+
|
78
|
+
config["components"].each do |component|
|
79
|
+
if component.is_a?(Hash)
|
80
|
+
component_name = component.fetch("name", component.inspect)
|
81
|
+
component_text = "Combined people configuration #{key.inspect} component #{component_name}"
|
82
|
+
Entitlements::Util::Util.validate_attr!(component_spec, component, component_text)
|
83
|
+
clazz = Entitlements::Data::People.class_for_config(component)
|
84
|
+
clazz.validate_config!("#{key}:#{component_name}", component.fetch("config"))
|
85
|
+
elsif component.is_a?(String)
|
86
|
+
if Entitlements.config.fetch("people", {}).fetch(component, nil)
|
87
|
+
resolved_component = Entitlements.config["people"][component]
|
88
|
+
clazz = Entitlements::Data::People.class_for_config(resolved_component)
|
89
|
+
clazz.validate_config!(component, resolved_component.fetch("config"))
|
90
|
+
else
|
91
|
+
raise ArgumentError, "In #{key}, reference to invalid component #{component.inspect}!"
|
92
|
+
end
|
93
|
+
else
|
94
|
+
raise ArgumentError, "In #{key}, expected array of hashes/strings but got #{component.inspect}!"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
|
101
|
+
# Constructor.
|
102
|
+
#
|
103
|
+
Contract C::KeywordArgs[
|
104
|
+
operator: String,
|
105
|
+
components: C::ArrayOf[C::HashOf[String => C::Any]]
|
106
|
+
] => C::Any
|
107
|
+
def initialize(operator:, components:)
|
108
|
+
@combined = { operator: operator }
|
109
|
+
@combined[:components] = components.map do |component|
|
110
|
+
clazz = Entitlements::Data::People.class_for_config(component)
|
111
|
+
clazz.new_from_config(component["config"])
|
112
|
+
end
|
113
|
+
@people = nil
|
114
|
+
end
|
115
|
+
|
116
|
+
# Read in the people from a combined provider. Cache result for later access.
|
117
|
+
#
|
118
|
+
# uid - Optionally a uid to return. If not specified, returns the entire hash.
|
119
|
+
#
|
120
|
+
# Returns Hash of { uid => Entitlements::Models::Person } or one Entitlements::Models::Person.
|
121
|
+
Contract C::Maybe[String] => C::Or[Entitlements::Models::Person, C::HashOf[String => Entitlements::Models::Person]]
|
122
|
+
def read(uid = nil)
|
123
|
+
@people ||= read_entire_hash
|
124
|
+
return @people unless uid
|
125
|
+
|
126
|
+
@people_downcase ||= @people.map { |people_uid, _data| [people_uid.downcase, people_uid] }.to_h
|
127
|
+
unless @people_downcase.key?(uid.downcase)
|
128
|
+
raise Entitlements::Data::People::NoSuchPersonError, "read(#{uid.inspect}) matched no known person"
|
129
|
+
end
|
130
|
+
|
131
|
+
@people[@people_downcase[uid.downcase]]
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
# Read an entire hash from the combined data source.
|
137
|
+
#
|
138
|
+
# Takes no arguments.
|
139
|
+
#
|
140
|
+
# Returns Hash of { uid => Entitlements::Models::Person }.
|
141
|
+
Contract C::None => C::HashOf[String => Entitlements::Models::Person]
|
142
|
+
def read_entire_hash
|
143
|
+
# @combined[:operator] is "or" or "and". Call the "read" method on each component and then assemble the
|
144
|
+
# results according to the specified logic. When a user is seen more than once, deconflict by using the *first*
|
145
|
+
# constructed person model that we have seen.
|
146
|
+
data = @combined[:components].map { |component| component.read }
|
147
|
+
|
148
|
+
result = {}
|
149
|
+
data.each do |data_hash|
|
150
|
+
data_hash.each do |user, user_data|
|
151
|
+
result[user] ||= user_data
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
if @combined[:operator] == "and"
|
156
|
+
users = Set.new(common_keys(data))
|
157
|
+
result.select! { |k, _| users.member?(k) }
|
158
|
+
end
|
159
|
+
|
160
|
+
result
|
161
|
+
end
|
162
|
+
|
163
|
+
# Given an arbitrary number of hashes, return the keys that are common in all of them.
|
164
|
+
# (hash1_keys & hash2_keys & hash3_keys)
|
165
|
+
#
|
166
|
+
# hashes - An array of hashes in which to find common keys.
|
167
|
+
#
|
168
|
+
# Returns an array of common elements.
|
169
|
+
Contract C::ArrayOf[Hash] => C::SetOf[C::Any]
|
170
|
+
def common_keys(hashes)
|
171
|
+
return Set.new if hashes.empty?
|
172
|
+
|
173
|
+
hash1 = hashes.shift
|
174
|
+
result = Set.new(hash1.keys)
|
175
|
+
hashes.each { |h| result = result & h.keys }
|
176
|
+
result
|
177
|
+
end
|
178
|
+
|
179
|
+
# Given an arbitrary number of hashes, return all keys seen in any of them.
|
180
|
+
# (hash1_keys | hash2_keys | hash3_keys)
|
181
|
+
#
|
182
|
+
# arrays - An array of arrays with elements in which to find any elements.
|
183
|
+
#
|
184
|
+
# Returns an array of all elements.
|
185
|
+
Contract C::ArrayOf[Hash] => C::SetOf[C::Any]
|
186
|
+
def all_keys(hashes)
|
187
|
+
return Set.new if hashes.empty?
|
188
|
+
|
189
|
+
hash1 = hashes.shift
|
190
|
+
result = Set.new(hash1.keys)
|
191
|
+
hashes.each { |h| result.merge(h.keys) }
|
192
|
+
result
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Entitlements
|
4
|
+
class Data
|
5
|
+
class People
|
6
|
+
class Dummy
|
7
|
+
include ::Contracts::Core
|
8
|
+
C = ::Contracts
|
9
|
+
|
10
|
+
# :nocov:
|
11
|
+
|
12
|
+
# Fingerprint for the object based on unique parameters from the group configuration. If the fingerprint
|
13
|
+
# matches the same object should be re-used. This will raise an error if insufficient configuration is
|
14
|
+
# given.
|
15
|
+
#
|
16
|
+
# config - Hash of configuration values as may be found in the Entitlements configuration file.
|
17
|
+
#
|
18
|
+
# Returns a String with the "fingerprint" for this configuration.
|
19
|
+
Contract C::HashOf[String => C::Any] => String
|
20
|
+
def self.fingerprint(_config)
|
21
|
+
"dummy"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Construct this object based on parameters in a group configuration. This is the direct translation
|
25
|
+
# between the Entitlements configuration file (which is always a Hash with configuration values) and
|
26
|
+
# the object constructed from this class (which can have whatever structure makes sense).
|
27
|
+
#
|
28
|
+
# config - Hash of configuration values as may be found in the Entitlements configuration file.
|
29
|
+
#
|
30
|
+
# Returns Entitlements::Data::People::LDAP object.
|
31
|
+
Contract C::HashOf[String => C::Any] => Entitlements::Data::People::Dummy
|
32
|
+
def self.new_from_config(_config)
|
33
|
+
new
|
34
|
+
end
|
35
|
+
|
36
|
+
# Validate configuration options.
|
37
|
+
#
|
38
|
+
# key - String with the name of the data source.
|
39
|
+
# config - Hash with the configuration data.
|
40
|
+
#
|
41
|
+
# Returns nothing.
|
42
|
+
Contract String, C::HashOf[String => C::Any] => nil
|
43
|
+
def self.validate_config!(_key, _config)
|
44
|
+
# This is always valid.
|
45
|
+
end
|
46
|
+
|
47
|
+
# Constructor.
|
48
|
+
#
|
49
|
+
# Takes no arguments.
|
50
|
+
Contract C::None => C::Any
|
51
|
+
def initialize
|
52
|
+
# This is pretty boring.
|
53
|
+
end
|
54
|
+
|
55
|
+
# This would normally read in all people and then return the hash or a specific person.
|
56
|
+
# In this case the hash is empty and there are no people.
|
57
|
+
#
|
58
|
+
# dn - Optionally a DN to return. If not specified, returns the entire hash.
|
59
|
+
#
|
60
|
+
# Returns empty hash or raises an error.
|
61
|
+
Contract C::Maybe[String] => C::Or[C::HashOf[String => Entitlements::Models::Person], Entitlements::Models::Person]
|
62
|
+
def read(dn = nil)
|
63
|
+
return {} if dn.nil?
|
64
|
+
raise Entitlements::Data::People::NoSuchPersonError, "read(#{dn.inspect}) matched no known person"
|
65
|
+
end
|
66
|
+
|
67
|
+
# :nocov:
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|