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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/VERSION +1 -0
  3. data/bin/deploy-entitlements +18 -0
  4. data/lib/entitlements/auditor/base.rb +163 -0
  5. data/lib/entitlements/backend/base_controller.rb +171 -0
  6. data/lib/entitlements/backend/base_provider.rb +55 -0
  7. data/lib/entitlements/backend/dummy/controller.rb +89 -0
  8. data/lib/entitlements/backend/dummy.rb +3 -0
  9. data/lib/entitlements/backend/ldap/controller.rb +188 -0
  10. data/lib/entitlements/backend/ldap/provider.rb +128 -0
  11. data/lib/entitlements/backend/ldap.rb +4 -0
  12. data/lib/entitlements/backend/member_of/controller.rb +203 -0
  13. data/lib/entitlements/backend/member_of.rb +3 -0
  14. data/lib/entitlements/cli.rb +121 -0
  15. data/lib/entitlements/data/groups/cached.rb +120 -0
  16. data/lib/entitlements/data/groups/calculated/base.rb +478 -0
  17. data/lib/entitlements/data/groups/calculated/filters/base.rb +93 -0
  18. data/lib/entitlements/data/groups/calculated/filters/member_of_group.rb +32 -0
  19. data/lib/entitlements/data/groups/calculated/modifiers/base.rb +38 -0
  20. data/lib/entitlements/data/groups/calculated/modifiers/expiration.rb +56 -0
  21. data/lib/entitlements/data/groups/calculated/ruby.rb +137 -0
  22. data/lib/entitlements/data/groups/calculated/rules/base.rb +35 -0
  23. data/lib/entitlements/data/groups/calculated/rules/group.rb +129 -0
  24. data/lib/entitlements/data/groups/calculated/rules/username.rb +41 -0
  25. data/lib/entitlements/data/groups/calculated/text.rb +337 -0
  26. data/lib/entitlements/data/groups/calculated/yaml.rb +171 -0
  27. data/lib/entitlements/data/groups/calculated.rb +290 -0
  28. data/lib/entitlements/data/groups.rb +13 -0
  29. data/lib/entitlements/data/people/combined.rb +197 -0
  30. data/lib/entitlements/data/people/dummy.rb +71 -0
  31. data/lib/entitlements/data/people/ldap.rb +142 -0
  32. data/lib/entitlements/data/people/yaml.rb +102 -0
  33. data/lib/entitlements/data/people.rb +58 -0
  34. data/lib/entitlements/extras/base.rb +40 -0
  35. data/lib/entitlements/extras/ldap_group/base.rb +20 -0
  36. data/lib/entitlements/extras/ldap_group/filters/member_of_ldap_group.rb +50 -0
  37. data/lib/entitlements/extras/ldap_group/rules/ldap_group.rb +69 -0
  38. data/lib/entitlements/extras/orgchart/base.rb +32 -0
  39. data/lib/entitlements/extras/orgchart/logic.rb +171 -0
  40. data/lib/entitlements/extras/orgchart/person_methods.rb +55 -0
  41. data/lib/entitlements/extras/orgchart/rules/direct_report.rb +62 -0
  42. data/lib/entitlements/extras/orgchart/rules/management.rb +59 -0
  43. data/lib/entitlements/extras.rb +82 -0
  44. data/lib/entitlements/models/action.rb +82 -0
  45. data/lib/entitlements/models/group.rb +280 -0
  46. data/lib/entitlements/models/person.rb +149 -0
  47. data/lib/entitlements/plugins/dummy.rb +22 -0
  48. data/lib/entitlements/plugins/group_of_names.rb +28 -0
  49. data/lib/entitlements/plugins/posix_group.rb +46 -0
  50. data/lib/entitlements/plugins.rb +13 -0
  51. data/lib/entitlements/rule/base.rb +74 -0
  52. data/lib/entitlements/service/ldap.rb +405 -0
  53. data/lib/entitlements/util/mirror.rb +42 -0
  54. data/lib/entitlements/util/override.rb +64 -0
  55. data/lib/entitlements/util/util.rb +219 -0
  56. data/lib/entitlements.rb +606 -0
  57. 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