entitlements 0.1.7

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