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