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,219 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Entitlements
|
4
|
+
class Util
|
5
|
+
class Util
|
6
|
+
include ::Contracts::Core
|
7
|
+
C = ::Contracts
|
8
|
+
|
9
|
+
# Downcase the first attribute of a distinguished name. This is used for case-insensitive
|
10
|
+
# matching in `member_strings` and elsewhere.
|
11
|
+
#
|
12
|
+
# dn - A String with a distinguished name in the format xxx=<name_to_downcase>,yyy
|
13
|
+
#
|
14
|
+
# Returns a String with the distinguished name, downcased.
|
15
|
+
Contract String => String
|
16
|
+
def self.downcase_first_attribute(dn)
|
17
|
+
return dn.downcase unless dn =~ /\A([^=]+)=([^,]+),(.+)\z/
|
18
|
+
"#{Regexp.last_match(1)}=#{Regexp.last_match(2).downcase},#{Regexp.last_match(3)}"
|
19
|
+
end
|
20
|
+
|
21
|
+
# If something looks like a distinguished name, obtain and return the first attribute.
|
22
|
+
# Otherwise return the input string.
|
23
|
+
#
|
24
|
+
# name_in - A String with either a name or a distinguished name
|
25
|
+
#
|
26
|
+
# Returns the name.
|
27
|
+
Contract String => String
|
28
|
+
def self.first_attr(name_in)
|
29
|
+
name_in =~ /\A[^=]+=([^,]+),/ ? Regexp.last_match(1) : name_in
|
30
|
+
end
|
31
|
+
|
32
|
+
# Given a hash, validate options for correct data type and presence of required attributes.
|
33
|
+
#
|
34
|
+
# spec - A Hash with the specification (see contract)
|
35
|
+
# data - A Hash with the actual options to test
|
36
|
+
# text - A description of the thing being validated, to print in error messages
|
37
|
+
#
|
38
|
+
# Returns nothing but may raise error.
|
39
|
+
Contract C::HashOf[String => { required: C::Bool, type: C::Or[Class, Array] }], C::HashOf[String => C::Any], String => nil
|
40
|
+
def self.validate_attr!(spec, data, text)
|
41
|
+
spec.each do |attr_name, config|
|
42
|
+
# Raise if required attribute is not present.
|
43
|
+
if config[:required] && !data.key?(attr_name)
|
44
|
+
raise "#{text} is missing attribute #{attr_name}!"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Skip the rest if the attribute isn't defined. (By the point the attribute is either
|
48
|
+
# present or it's optional.)
|
49
|
+
next unless data.key?(attr_name)
|
50
|
+
|
51
|
+
# Make sure the attribute has the proper data type. Return when a match occurs.
|
52
|
+
correct_type = [config[:type]].flatten.select { |type| data[attr_name].is_a?(type) }
|
53
|
+
unless correct_type.any?
|
54
|
+
existing = data[attr_name].class.to_s
|
55
|
+
expected = [config[:type]].flatten.map { |clazz| clazz.to_s }.join(", ")
|
56
|
+
raise "#{text} attribute #{attr_name.inspect} is supposed to be #{config[:type]}, not #{existing}!"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
extra_keys = data.keys - spec.keys
|
61
|
+
if extra_keys.any?
|
62
|
+
extra_keys_text = extra_keys.join(", ")
|
63
|
+
raise "#{text} contains unknown attribute(s): #{extra_keys_text}"
|
64
|
+
end
|
65
|
+
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
# From a group's key, get the directory where that group's files are defined. Normally this is
|
70
|
+
# the entitlements path concatenated with the group's key, but it can be overridden with a "dir"
|
71
|
+
# attribute on the group.
|
72
|
+
#
|
73
|
+
# group - A String with the key of the group as per the configuration file.
|
74
|
+
#
|
75
|
+
# Returns a String with the full directory path to the group.
|
76
|
+
Contract String => String
|
77
|
+
def self.path_for_group(group)
|
78
|
+
unless Entitlements.config["groups"].key?(group)
|
79
|
+
raise ArgumentError, "path_for_group: Group #{group.inspect} is not defined in the entitlements configuration!"
|
80
|
+
end
|
81
|
+
|
82
|
+
dir = Entitlements.config["groups"][group]["dir"]
|
83
|
+
result_dir = if dir.nil?
|
84
|
+
File.join(Entitlements.config_path, group)
|
85
|
+
elsif dir.start_with?("/")
|
86
|
+
dir
|
87
|
+
else
|
88
|
+
File.expand_path(dir, Entitlements.config_path)
|
89
|
+
end
|
90
|
+
|
91
|
+
return result_dir if File.directory?(result_dir)
|
92
|
+
raise Errno::ENOENT, "Non-existing directory #{result_dir.inspect} for group #{group.inspect}!"
|
93
|
+
end
|
94
|
+
|
95
|
+
# Get the common name from the distinguished name from either a String or an Entitlements::Models::Group.
|
96
|
+
#
|
97
|
+
# obj - Either a String (in DN format) or an Entitlements::Models::Group object.
|
98
|
+
#
|
99
|
+
# Returns a String with the common name.
|
100
|
+
Contract C::Any => String
|
101
|
+
def self.any_to_cn(obj)
|
102
|
+
if obj.is_a?(Entitlements::Models::Group)
|
103
|
+
return obj.cn.downcase
|
104
|
+
end
|
105
|
+
|
106
|
+
if obj.is_a?(String) && obj.start_with?("cn=")
|
107
|
+
return Entitlements::Util::Util.first_attr(obj).downcase
|
108
|
+
end
|
109
|
+
|
110
|
+
if obj.is_a?(String)
|
111
|
+
return obj
|
112
|
+
end
|
113
|
+
|
114
|
+
message = "Could not determine a common name from #{obj.inspect}!"
|
115
|
+
raise ArgumentError, message
|
116
|
+
end
|
117
|
+
|
118
|
+
# Given an Array or a Set of uids or distinguished name, and a set of uids to be removed, delete any matching
|
119
|
+
# uids from the original object in a case-insensitive matter. Compares simple strings, distinguished names, etc.
|
120
|
+
#
|
121
|
+
# obj - A supported Enumerable or Entitlements::Models::Group (will be mutated)
|
122
|
+
# uids - A Set of Strings with the uid(s) to be removed - uid(s) must all be lower case!
|
123
|
+
#
|
124
|
+
# Returns nothing but mutates `obj`.
|
125
|
+
Contract C::Or[C::SetOf[String], C::ArrayOf[String]], C::Or[nil, C::SetOf[String]] => C::Any
|
126
|
+
def self.remove_uids(obj, uids)
|
127
|
+
return unless uids
|
128
|
+
obj.delete_if do |uid|
|
129
|
+
uids.member?(uid.downcase) || uids.member?(Entitlements::Util::Util.first_attr(uid).downcase)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Convert a string into CamelCase.
|
134
|
+
#
|
135
|
+
# str - The string that needs to be converted to CamelCase.
|
136
|
+
#
|
137
|
+
# Returns a String in CamelCase.
|
138
|
+
Contract String => String
|
139
|
+
def self.camelize(str)
|
140
|
+
result = str.split(/[\W_]+/).collect! { |w| w.capitalize }.join
|
141
|
+
|
142
|
+
# Special cases
|
143
|
+
result.gsub("Github", "GitHub").gsub("Ldap", "LDAP")
|
144
|
+
end
|
145
|
+
|
146
|
+
# Convert CamelCase back into an identifier string.
|
147
|
+
#
|
148
|
+
# str - The CamelCase string to be converted to identifier_case.
|
149
|
+
#
|
150
|
+
# Returns a String.
|
151
|
+
Contract String => String
|
152
|
+
def self.decamelize(str)
|
153
|
+
str.gsub("GitHub", "Github").gsub("LDAP", "Ldap").gsub(/([a-z\d])([A-Z])/, "\\1_\\2").downcase
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns a date object from the given input object.
|
157
|
+
#
|
158
|
+
# input - Any type of object that will be parsed as a date.
|
159
|
+
#
|
160
|
+
# Returns a date object.
|
161
|
+
Contract C::Any => Date
|
162
|
+
def self.parse_date(input)
|
163
|
+
if input.is_a?(Date)
|
164
|
+
return input
|
165
|
+
end
|
166
|
+
|
167
|
+
if input.is_a?(String)
|
168
|
+
if input =~ /\A(\d{4})-?(\d{2})-?(\d{2})\z/
|
169
|
+
return Date.new(Regexp.last_match(1).to_i, Regexp.last_match(2).to_i, Regexp.last_match(3).to_i)
|
170
|
+
end
|
171
|
+
|
172
|
+
raise ArgumentError, "Unsupported date format #{input.inspect} for parse_date!"
|
173
|
+
end
|
174
|
+
|
175
|
+
raise ArgumentError, "Unsupported object #{input.inspect} for parse_date!"
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns the absolute path to a file or directory. If the filename starts with "/" then that is the absolute
|
179
|
+
# path. Otherwise the path returned is relative to the location of the Entitlements configuration file.
|
180
|
+
#
|
181
|
+
# path - String with the input path
|
182
|
+
#
|
183
|
+
# Returns a String with the full path.
|
184
|
+
Contract String => String
|
185
|
+
def self.absolute_path(path)
|
186
|
+
return path if path.start_with?("/")
|
187
|
+
entitlements_config_dirname = File.dirname(Entitlements.config_file)
|
188
|
+
File.expand_path(path, entitlements_config_dirname)
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.dns_for_ou(ou, cfg_obj)
|
192
|
+
results = []
|
193
|
+
path = path_for_group(ou)
|
194
|
+
Dir.glob(File.join(path, "*")).each do |filename|
|
195
|
+
# If it's a directory, skip it for now.
|
196
|
+
if File.directory?(filename)
|
197
|
+
next
|
198
|
+
end
|
199
|
+
|
200
|
+
# If the file is ignored (e.g. documentation) then skip it.
|
201
|
+
if Entitlements::IGNORED_FILES.member?(File.basename(filename))
|
202
|
+
next
|
203
|
+
end
|
204
|
+
|
205
|
+
# Determine the group DN. The CN will be the filname without its extension.
|
206
|
+
file_without_extension = File.basename(filename).sub(/\.\w+\z/, "")
|
207
|
+
unless file_without_extension =~ /\A[\w\-]+\z/
|
208
|
+
raise "Illegal LDAP group name #{file_without_extension.inspect} in #{ou}!"
|
209
|
+
end
|
210
|
+
group_dn = ["cn=#{file_without_extension}", cfg_obj.fetch("base")].join(",")
|
211
|
+
|
212
|
+
results << group_dn
|
213
|
+
end
|
214
|
+
|
215
|
+
results
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|