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,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Filter class to remove members of a particular Entitlements-managed group.
4
+
5
+ module Entitlements
6
+ class Data
7
+ class Groups
8
+ class Calculated
9
+ class Filters
10
+ class MemberOfGroup < Entitlements::Data::Groups::Calculated::Filters::Base
11
+ include ::Contracts::Core
12
+ C = ::Contracts
13
+
14
+ # Determine if the member is filtered as per this definition. Return true if the member
15
+ # is to be filtered out, false if the member does not match the filter.
16
+ #
17
+ # member - Entitlements::Models::Person object
18
+ #
19
+ # Returns true if the person is to be filtered out, false otherwise.
20
+ Contract Entitlements::Models::Person => C::Bool
21
+ def filtered?(member)
22
+ return false if filter == :all
23
+ return false unless member_of_named_group?(member, config.fetch("group"))
24
+ return true if filter == :none
25
+ !member_of_filter?(member)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Entitlements
4
+ class Data
5
+ class Groups
6
+ class Calculated
7
+ class Modifiers
8
+ class Base
9
+ include ::Contracts::Core
10
+ C = ::Contracts
11
+
12
+ # Constructor. Needs the cache (Hash with various objects of interest) for
13
+ # future lookups.
14
+ #
15
+ # rs - Entitlements::Data::Groups::Calculated::* object
16
+ # config - Configuration for this modifier as defined in entitlement
17
+ Contract C::KeywordArgs[
18
+ rs: C::Or[
19
+ Entitlements::Data::Groups::Calculated::Ruby,
20
+ Entitlements::Data::Groups::Calculated::Text,
21
+ Entitlements::Data::Groups::Calculated::YAML,
22
+ ],
23
+ config: C::Any
24
+ ] => C::Any
25
+ def initialize(rs:, config: nil)
26
+ @rs = rs
27
+ @config = config
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :config, :rs
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../../../../util/util"
5
+
6
+ module Entitlements
7
+ class Data
8
+ class Groups
9
+ class Calculated
10
+ class Modifiers
11
+ class Expiration < Base
12
+ include ::Contracts::Core
13
+ C = ::Contracts
14
+
15
+ # Given a set of members in a group that is being calculated, modify the
16
+ # member set (the input) to be empty if the entitlement as a whole is expired.
17
+ # If we do this, add the metadata that allows an empty group, assuming there
18
+ # were in fact members in the group the first time we saw this.
19
+ #
20
+ # result - Set of Entitlements::Models::Person (mutated).
21
+ #
22
+ # Return true if we made any changes, false otherwise.
23
+ Contract C::SetOf[Entitlements::Models::Person] => C::Bool
24
+ def modify(result)
25
+ # If group is already empty, we have nothing to consider modifying, regardless
26
+ # of expiration date. Just return false right away.
27
+ if result.empty?
28
+ return false
29
+ end
30
+
31
+ # If the date is in the future, leave the entitlement unchanged.
32
+ return false if parse_date > Time.now.utc.to_date
33
+
34
+ # Empty the group. Set metadata allowing no members. Return true to indicate modification.
35
+ rs.metadata["no_members_ok"] = true
36
+ result.clear
37
+ true
38
+ end
39
+
40
+ private
41
+
42
+ # Returns a date object from the configuration given to the class.
43
+ #
44
+ # Takes no arguments.
45
+ #
46
+ # Returns a date object.
47
+ Contract C::None => Date
48
+ def parse_date
49
+ Entitlements::Util::Util.parse_date(config)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+ # Interact with rules that are stored as ruby code.
3
+
4
+ module Entitlements
5
+ class Data
6
+ class Groups
7
+ class Calculated
8
+ class Ruby < Entitlements::Data::Groups::Calculated::Base
9
+ include ::Contracts::Core
10
+ C = ::Contracts
11
+
12
+ # Standard interface: Calculate the members of this group.
13
+ #
14
+ # Takes no arguments.
15
+ #
16
+ # Returns a Set[Entitlements::Models::Person] with DN's of the people in the group.
17
+ Contract C::None => C::SetOf[Entitlements::Models::Person]
18
+ def members
19
+ @members ||= begin
20
+ Entitlements.logger.debug "Calculating members from #{filename}"
21
+ result = rule_obj.members
22
+
23
+ # Since this is user-written code not subject to contracts, do some basic
24
+ # format checking of the result, and standardize the output.
25
+ unless result.is_a?(Set)
26
+ raise "Expected Set[String|Entitlements::Models::Person] from #{ruby_class_name}.members, got #{result.class}!"
27
+ end
28
+
29
+ cleaned_set = result.map do |item|
30
+ if item.is_a?(String)
31
+ begin
32
+ Entitlements.cache[:people_obj].read.fetch(item)
33
+ rescue KeyError => exc
34
+ raise_rule_exception(exc)
35
+ end
36
+ elsif item.is_a?(Entitlements::Models::Person)
37
+ item
38
+ else
39
+ raise "In #{ruby_class_name}.members, expected String or Person but got #{item.inspect}"
40
+ end
41
+ end
42
+
43
+ # All good, return the result.
44
+ Set.new(cleaned_set)
45
+ end
46
+ end
47
+
48
+ # Standard interface: Get the description of this group.
49
+ #
50
+ # Takes no arguments.
51
+ #
52
+ # Returns a String with the group description, or "" if undefined.
53
+ Contract C::None => String
54
+ def description
55
+ result = rule_obj.description
56
+ return result if result.is_a?(String)
57
+ raise "Expected String from #{ruby_class_name}.description, got #{result.class}!"
58
+ end
59
+
60
+ private
61
+
62
+ # Get a hash of the filters defined in the group.
63
+ #
64
+ # Takes no arguments.
65
+ #
66
+ # Returns a Hash[String => :all/:none/List of strings].
67
+ Contract C::None => C::HashOf[String => C::Or[:all, :none, C::ArrayOf[String]]]
68
+ def initialize_filters
69
+ rule_obj.filters
70
+ end
71
+
72
+ # Files can support metadata intended for consumption by things other than LDAP.
73
+ # This returns the metadata from the file as a hash.
74
+ #
75
+ # Takes no arguments.
76
+ #
77
+ # Returns Hash[<String>key => <Object>value]
78
+ Contract C::None => C::HashOf[String => C::Any]
79
+ def initialize_metadata
80
+ return {} unless rule_obj.respond_to?(:metadata)
81
+
82
+ result = rule_obj.metadata
83
+
84
+ unless result.is_a?(Hash)
85
+ raise ArgumentError, "For metadata in #{filename}: expected Hash, got #{result.inspect}!"
86
+ end
87
+
88
+ result.each do |key, _|
89
+ next if key.is_a?(String)
90
+ raise ArgumentError, "For metadata in #{filename}: keys are expected to be strings, but #{key.inspect} is not!"
91
+ end
92
+
93
+ result
94
+ end
95
+
96
+ # Instantiate the object exactly once, on demand. Cache it for later.
97
+ #
98
+ # Takes no arguments.
99
+ #
100
+ # Returns an object.
101
+ Contract C::None => Object
102
+ def rule_obj
103
+ @rule_obj ||= begin
104
+ require filename
105
+ clazz = Kernel.const_get(ruby_class_name)
106
+ clazz.new
107
+ end
108
+ end
109
+
110
+ # Raise an exception which adds in the offending class name and filename (which
111
+ # is a user-defined entitlement that gave rise to a problem).
112
+ #
113
+ # exc - An Exception that is to be raised.
114
+ #
115
+ # Returns nothing (raises the exception after logging).
116
+ Contract Exception => nil
117
+ def raise_rule_exception(exc)
118
+ Entitlements.logger.fatal "#{exc.class} when processing #{ruby_class_name}!"
119
+ raise exc
120
+ end
121
+
122
+ # Turn the filename into a ruby class name. We care about the name of the last
123
+ # directory, and the name of the file itself. Convert these into CamelCase for
124
+ # the ruby class.
125
+ #
126
+ # Takes no arguments.
127
+ #
128
+ # Returns a String with the class name.
129
+ Contract C::None => String
130
+ def ruby_class_name
131
+ ["Entitlements", "Rule", ou, cn].map { |x| camelize(x) }.join("::")
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ # Base class for rules that we write.
3
+
4
+ module Entitlements
5
+ class Data
6
+ class Groups
7
+ class Calculated
8
+ class Rules
9
+ class Base
10
+ include ::Contracts::Core
11
+ C = ::Contracts
12
+
13
+ # Interface method: Get a Set[Entitlements::Models::Person] matching this condition.
14
+ #
15
+ # value - The value to match.
16
+ # filename - Name of the file resulting in this rule being called
17
+ # options - Optional hash of additional method-specific options
18
+ #
19
+ # Returns a Set[Entitlements::Models::Person].
20
+ Contract C::KeywordArgs[
21
+ value: String,
22
+ filename: C::Maybe[String],
23
+ options: C::Optional[C::HashOf[Symbol => C::Any]]
24
+ ] => C::SetOf[Entitlements::Models::Person]
25
+ def self.matches(value:, filename: nil, options: {})
26
+ # :nocov:
27
+ raise "matches() must be defined in the child class #{self.class}!"
28
+ # :nocov:
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+ # Is someone in an LDAP group?
3
+
4
+ module Entitlements
5
+ class Data
6
+ class Groups
7
+ class Calculated
8
+ class Rules
9
+ class Group < Entitlements::Data::Groups::Calculated::Rules::Base
10
+
11
+ FILE_EXTENSIONS = {
12
+ "rb" => "Entitlements::Data::Groups::Calculated::Ruby",
13
+ "txt" => "Entitlements::Data::Groups::Calculated::Text",
14
+ "yaml" => "Entitlements::Data::Groups::Calculated::YAML"
15
+ }
16
+
17
+ # Interface method: Get a Set[Entitlements::Models::Person] matching this condition.
18
+ #
19
+ # value - The value to match.
20
+ # filename - Name of the file resulting in this rule being called
21
+ # options - Optional hash of additional method-specific options
22
+ #
23
+ # Returns a Set[Entitlements::Models::Person].
24
+ Contract C::KeywordArgs[
25
+ value: String,
26
+ filename: C::Maybe[String],
27
+ options: C::Optional[C::HashOf[Symbol => C::Any]]
28
+ ] => C::SetOf[Entitlements::Models::Person]
29
+ def self.matches(value:, filename: nil, options: {})
30
+ # We've asked for a managed group, so we need to calculate that group and return its members.
31
+ # First parse the value into the ou and cn.
32
+ raise "Error: Unexpected value #{value.inspect} in #{self.class}!" unless value =~ %r{\A(.+)/([^/]+)\z}
33
+ ou = value.rpartition("/").first
34
+ cn = value.rpartition("/").last
35
+
36
+ # Check the cache. If we are in the process of calculating it, it's a circular dependency that should be alerted upon.
37
+ # If we've already calculated this, just return the value.
38
+ current_value = Entitlements.cache[:calculated].fetch(ou, {}).fetch(cn, nil)
39
+ if current_value == :calculating
40
+ Entitlements.cache[:dependencies] << "#{ou}/#{cn}"
41
+ raise "Error: Circular dependency #{Entitlements.cache[:dependencies].join(' -> ')}"
42
+ end
43
+
44
+ # If we have calculated this before, then apply modifiers and return the result. `current_value` here
45
+ # is a set with the correct answers but that does not take into effect any modifiers. Therefore we
46
+ # reference back to the object so we can have the modifiers applied. There's a cache in the object that
47
+ # remembers the value of `.modified_members` each time it's calculated, so this is inexpensive.
48
+ filebase_with_path = File.join(Entitlements::Util::Util.path_for_group(ou), cn)
49
+ if Entitlements.cache[:file_objects].key?(filebase_with_path)
50
+ return Entitlements.cache[:file_objects][filebase_with_path].modified_members
51
+ end
52
+
53
+ # We actually need to calculate this group. Find the file based on the ou and cn in the directory.
54
+ files = files_for(ou, options: options)
55
+ match_regex = Regexp.new("\\A" + Regexp.escape(cn.gsub("*", "\f")).gsub("\\f", ".*") + "\\z")
56
+ matching_files = files.select { |base, _ext| match_regex.match(base) }
57
+ if matching_files.any?
58
+ result = Set.new
59
+ matching_files.each do |filebase, ext|
60
+ filebase_with_path = File.join(Entitlements::Util::Util.path_for_group(ou), filebase)
61
+
62
+ # If the object has already been calculated then we can just merge the value from
63
+ # the cache without going any further. Otherwise, create a new object for the group
64
+ # reference and calculate them.
65
+ unless Entitlements.cache[:file_objects][filebase_with_path]
66
+ clazz = Kernel.const_get(FILE_EXTENSIONS[ext])
67
+ Entitlements.cache[:file_objects][filebase_with_path] = clazz.new(
68
+ filename: "#{filebase_with_path}.#{ext}",
69
+ )
70
+ if Entitlements.cache[:file_objects][filebase_with_path].members == :calculating
71
+ next if matching_files.size > 1
72
+ raise "Error: Invalid self-referencing wildcard in #{ou}/#{filebase}.#{ext}"
73
+ end
74
+ end
75
+
76
+ unless Entitlements.cache[:file_objects][filebase_with_path].modified_members == :calculating
77
+ result.merge Entitlements.cache[:file_objects][filebase_with_path].modified_members
78
+ end
79
+ end
80
+ return result
81
+ end
82
+
83
+ # No file exists... handle accordingly
84
+ path = File.join(Entitlements::Util::Util.path_for_group(ou), "#{cn}.(rb|txt|yaml)")
85
+ if options[:skip_broken_references]
86
+ Entitlements.logger.warn "Could not find a configuration for #{path} - skipped (filename: #{filename.inspect})"
87
+ return Set.new
88
+ end
89
+
90
+ Entitlements.logger.fatal "Error: Could not find a configuration for #{path} (filename: #{filename.inspect})"
91
+ raise "Error: Could not find a configuration for #{path} (filename: #{filename.inspect})"
92
+ end
93
+
94
+ # Enumerate and cache all files in a directory for more efficient processing later.
95
+ #
96
+ # path - A String with the directory structure relative to Entitlements.config_path
97
+ #
98
+ # Returns a Set of Hashes with { "file_without_extension" => "extension" }
99
+ Contract String, C::KeywordArgs[options: C::HashOf[Symbol => C::Any]] => C::HashOf[String => String]
100
+ def self.files_for(path, options:)
101
+ @files_for_cache ||= {}
102
+ @files_for_cache[path] ||= begin
103
+ full_path = Entitlements::Util::Util.path_for_group(path)
104
+ if File.directory?(full_path)
105
+ result = {}
106
+ Dir.entries(full_path).each do |name|
107
+ next if name.start_with?(".")
108
+ next unless name.end_with?(*FILE_EXTENSIONS.keys.map { |k| ".#{k}" })
109
+ next unless File.file?(File.join(full_path, name))
110
+ raise "Unparseable name: #{full_path}/#{name}" unless name =~ /\A(.+)\.(\w+)\z/
111
+ result[Regexp.last_match(1)] = Regexp.last_match(2)
112
+ end
113
+ result
114
+ elsif options[:skip_broken_references]
115
+ Entitlements.logger.warn "Could not find any configuration in #{full_path} - skipped"
116
+ {}
117
+ else
118
+ message = "Error: Could not find any configuration in #{full_path}"
119
+ Entitlements.logger.fatal message
120
+ raise RuntimeError, message
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ # The simplest equality check we could imagine.
3
+
4
+ module Entitlements
5
+ class Data
6
+ class Groups
7
+ class Calculated
8
+ class Rules
9
+ class Username < Entitlements::Data::Groups::Calculated::Rules::Base
10
+ include ::Contracts::Core
11
+ C = ::Contracts
12
+
13
+ # Interface method: Get a Set[Entitlements::Models::Person] matching this condition.
14
+ #
15
+ # value - The value to match.
16
+ # filename - Name of the file resulting in this rule being called
17
+ # options - Optional hash of additional method-specific options
18
+ #
19
+ # Returns a Set[Entitlements::Models::Person].
20
+ Contract C::KeywordArgs[
21
+ value: String,
22
+ filename: C::Maybe[String],
23
+ options: C::Optional[C::HashOf[Symbol => C::Any]]
24
+ ] => C::SetOf[Entitlements::Models::Person]
25
+ def self.matches(value:, filename: nil, options: {})
26
+ # Username is easy - the value is the uid.
27
+ begin
28
+ Set.new([Entitlements.cache[:people_obj].read(value)].compact)
29
+ rescue Entitlements::Data::People::NoSuchPersonError
30
+ # We are not currently treating this as a fatal error and as such we are just
31
+ # ignoring it. Implementors will want to implement CI-level checks for unknown
32
+ # people if this is a concern.
33
+ Set.new({})
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end