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