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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e129c20434bdad80c9fb7395a3a18f787f3a6c3c7c5579254a2bcd1b422e2715
4
+ data.tar.gz: eee34c03695d06d79c5eec2dc8e98627a0c66e5f9c4425c60d1ba247f9541f22
5
+ SHA512:
6
+ metadata.gz: fc823d7ff8a6c1c6cdc582526e5bd4f3fa67d48ae6acc9940c37e15073c6661ca33469b85e5e12e4e6dee7981b4ef3022225c0697b1e449b1c8f4a98f524f9cf
7
+ data.tar.gz: 44724ff2988f4a7ca36b20489ace98d0af6351e8214d2975a1e256cfc088a5a4070c91524ed49342e684d5edc7bc7eb41d59dba8788000dfefef8566b653d3bb
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.7
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ENV["BUNDLE_GEMFILE"] = File.expand_path("../Gemfile", File.dirname(__FILE__))
4
+ require "bundler/setup"
5
+ require "contracts"
6
+
7
+ # We don't need Contract outside of normal development
8
+ VALID = [true, nil]
9
+ class Contract
10
+ def self.valid?(arg, contract)
11
+ VALID
12
+ end
13
+ end
14
+
15
+ require "entitlements"
16
+ exitcode = Entitlements::Cli.run
17
+ exitcode ||= 0
18
+ exit exitcode
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class provides common methods and is intended to be inherited by other audit providers.
4
+
5
+ module Entitlements
6
+ class Auditor
7
+ class Base
8
+ include ::Contracts::Core
9
+ C = ::Contracts
10
+
11
+ attr_reader :description, :provider_id
12
+
13
+ # ---------
14
+ # Interface
15
+ # ---------
16
+
17
+ # Constructor.
18
+ #
19
+ # config - A Hash with configuration options
20
+ Contract Logger, C::HashOf[String => C::Any] => C::Any
21
+ def initialize(logger, config)
22
+ @logger = logger
23
+ @description = config["description"] || self.class.to_s
24
+ @provider_id = config["provider_id"] || self.class.to_s.split("::").last
25
+ @config = config
26
+ end
27
+
28
+ # Setup. This sets up the audit provider before any action takes place. This may be
29
+ # declared in the child class.
30
+ #
31
+ # Takes no arguments.
32
+ #
33
+ # Returns nothing.
34
+ Contract C::None => nil
35
+ def setup
36
+ # :nocov:
37
+ nil
38
+ # :nocov:
39
+ end
40
+
41
+ # Commit. This takes the entirety of group objects and actions and records them in
42
+ # whatever methodology the audit provider uses.
43
+ #
44
+ # actions - Array of Entitlements::Models::Action (all requested actions)
45
+ # successful_actions - Array of Entitlements::Models::Action (successfully applied actions)
46
+ # provider_exception - Exception raised by a provider when applying (hopefully nil)
47
+ #
48
+ # Returns nothing.
49
+ Contract C::KeywordArgs[
50
+ actions: C::ArrayOf[Entitlements::Models::Action],
51
+ successful_actions: C::ArrayOf[Entitlements::Models::Action],
52
+ provider_exception: C::Or[nil, Exception]
53
+ ] => nil
54
+ def commit(actions:, successful_actions:, provider_exception:)
55
+ # :nocov:
56
+ nil
57
+ # :nocov:
58
+ end
59
+
60
+ # Set up a logger class that wraps incoming messages with the prefix and (if meaningful) the
61
+ # provider ID. Messages are then sent with the requested priority to the actual logger object.
62
+ class CustomLogger
63
+ def initialize(underlying_object, underlying_logger)
64
+ @underlying_object = underlying_object
65
+ @underlying_logger = underlying_logger
66
+ end
67
+
68
+ def prefix
69
+ @prefix ||= begin
70
+ if @underlying_object.provider_id == @underlying_object.class.to_s.split("::").last
71
+ @underlying_object.class.to_s
72
+ else
73
+ "#{@underlying_object.class}[#{@underlying_object.provider_id}]"
74
+ end
75
+ end
76
+ end
77
+
78
+ def method_missing(m, *args, &block)
79
+ args[0] = "#{prefix}: #{args.first}"
80
+ @underlying_logger.send(m, *args, &block)
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ attr_reader :config
87
+
88
+ # Intercept calls to logger to wrap through the custom class.
89
+ def logger
90
+ @logger_class ||= CustomLogger.new(self, @logger)
91
+ end
92
+
93
+ # Raise a configuration error message.
94
+ #
95
+ # message - A String with the error message to be logged and raised.
96
+ #
97
+ # Returns nothing because it raises an error.
98
+ Contract String => C::Any
99
+ def configuration_error(message)
100
+ provider = self.class.to_s.split("::").last
101
+ error_message = "Configuration error for provider=#{provider} id=#{provider_id}: #{message}"
102
+ logger.fatal "Configuration error: #{message}"
103
+ raise ArgumentError, error_message
104
+ end
105
+
106
+ # Require the the configuration contain certain keys (no validation is performed on the
107
+ # values - just make sure the key exists).
108
+ #
109
+ # required_keys - An Array of Strings with the required keys.
110
+ #
111
+ # Returns nothing.
112
+ Contract C::ArrayOf[String] => nil
113
+ def require_config_keys(required_keys)
114
+ missing_keys = required_keys - config.keys
115
+ return unless missing_keys.any?
116
+ configuration_error "Not all required keys are defined. Missing: #{missing_keys.join(',')}."
117
+ end
118
+
119
+ # Convert a distinguished name (cn=something,ou=foo,dc=example,dc=net) to a file
120
+ # path (dc=net/dc=example/ou=foo/cn=something).
121
+ #
122
+ # dn - A String with the distinguished name.
123
+ #
124
+ # Returns a String with the path.
125
+ Contract String => String
126
+ def path_from_dn(dn)
127
+ File.join(dn.split(",").reverse)
128
+ end
129
+
130
+ # Convert a path (dc=net/dc=example/ou=foo/cn=something) to a distinguished name
131
+ # (cn=something,ou=foo,dc=example,dc=net).
132
+ #
133
+ # path - A String with the path name.
134
+ #
135
+ # Returns a String with the distinguished name.
136
+ Contract String => String
137
+ def dn_from_path(path)
138
+ path.split("/").reject { |i| i.empty? }.reverse.join(",")
139
+ end
140
+
141
+ # From a list of actions, return only the ones that have a net change in membership.
142
+ # In other words, filter out the ones where only metadata / description / etc. has changed.
143
+ #
144
+ # actions - Incoming array of Entitlements::Models::Action
145
+ #
146
+ # Returns an array of Entitlements::Models::Action
147
+ Contract C::ArrayOf[Entitlements::Models::Action] => C::ArrayOf[Entitlements::Models::Action]
148
+ def actions_with_membership_change(actions)
149
+ actions.select do |action|
150
+ if action.updated.is_a?(Entitlements::Models::Person) || action.existing == :none
151
+ # MemberOf or other modification to the person itself, not handled by this auditor
152
+ false
153
+ elsif action.updated.nil? || action.existing.nil?
154
+ # Add/remove group always triggers a commit
155
+ true
156
+ else
157
+ action.updated.member_strings_insensitive != action.existing.member_strings_insensitive
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ # To add a new backend, make its "Controller" class inherit from Entitlements::Backend::BaseController.
4
+ # Consider using dummy/controller.rb as a template for a brand new class.
5
+
6
+ # Needed to register backends
7
+ require_relative "../cli"
8
+ require_relative "../util/util"
9
+
10
+ module Entitlements
11
+ class Backend
12
+ class BaseController
13
+ include ::Contracts::Core
14
+ C = ::Contracts
15
+
16
+ # Upon loading of the class itself, register the class in the list of available
17
+ # backends that is tracked in the Entitlements class.
18
+ def self.register
19
+ Entitlements.register_backend(identifier, self, priority)
20
+ end
21
+
22
+ # Default priority is 10 - override by defining this method in the child class.
23
+ def self.priority
24
+ 10
25
+ end
26
+
27
+ # :nocov:
28
+ def priority
29
+ self.class.priority
30
+ end
31
+ # :nocov:
32
+
33
+ # Default identifier is the de-camelized name of the class - override by defining this method in the child class.
34
+ def self.identifier
35
+ classname = self.to_s.split("::")[-2]
36
+ Entitlements::Util::Util.decamelize(classname)
37
+ end
38
+
39
+ COMMON_GROUP_CONFIG = {
40
+ "allowed_methods" => { required: false, type: Array },
41
+ "allowed_types" => { required: false, type: Array },
42
+ "dir" => { required: false, type: String }
43
+ }
44
+
45
+ # Constructor. Generic constructor that takes a hash of configuration options.
46
+ #
47
+ # group_name - Name of the corresponding group in the entitlements configuration file.
48
+ # config - Optionally, a Hash of configuration information (configuration is referenced if empty).
49
+ Contract String, C::Maybe[C::HashOf[String => C::Any]] => C::Any
50
+ def initialize(group_name, config = nil)
51
+ @group_name = group_name
52
+ @config = config ? config.dup : Entitlements.config["groups"].fetch(group_name).dup
53
+ @config.delete("type")
54
+ @actions = []
55
+ @logger = Entitlements.logger
56
+ validate_config!(@group_name, @config)
57
+ end
58
+
59
+ attr_reader :actions
60
+
61
+ # Print difference array.
62
+ #
63
+ # key - String with the key identifying the OU
64
+ # added - Array[Entitlements::Models::Action]
65
+ # removed - Array[Entitlements::Models::Action]
66
+ # changed - Array[Entitlements::Models::Action]
67
+ # ignored_users - Optionally a Set of Strings with usernames to ignore
68
+ #
69
+ # Returns nothing (this just prints to logger).
70
+ Contract C::KeywordArgs[
71
+ key: String,
72
+ added: C::ArrayOf[Entitlements::Models::Action],
73
+ removed: C::ArrayOf[Entitlements::Models::Action],
74
+ changed: C::ArrayOf[Entitlements::Models::Action],
75
+ ignored_users: C::Maybe[C::SetOf[String]]
76
+ ] => C::Any
77
+ def print_differences(key:, added:, removed:, changed:, ignored_users: Set.new)
78
+ added_array = added.map { |i| [i.dn, :added, i] }
79
+ removed_array = removed.map { |i| [i.dn, :removed, i] }
80
+ changed_array = changed.map { |i| [i.dn, :changed, i] }
81
+
82
+ combined = (added_array + removed_array + changed_array).sort_by { |i| i.first.to_s.downcase }
83
+ combined.each do |entry|
84
+ identifier = entry[0]
85
+ changetype = entry[1]
86
+ obj = entry[2]
87
+
88
+ if changetype == :added
89
+ members = obj.updated.member_strings.map { |i| i =~ /\Auid=(.+?),/ ? Regexp.last_match(1) : i }
90
+ Entitlements.logger.info "ADD #{identifier} to #{key} (Members: #{members.sort.join(',')})"
91
+ elsif changetype == :removed
92
+ Entitlements.logger.info "DELETE #{identifier} from #{key}"
93
+ else
94
+ ignored_users.merge obj.ignored_users
95
+ existing_members = obj.existing.member_strings
96
+ Entitlements::Util::Util.remove_uids(existing_members, ignored_users)
97
+
98
+ proposed_members = obj.updated.member_strings
99
+ Entitlements::Util::Util.remove_uids(proposed_members, ignored_users)
100
+
101
+ added_to_group = (proposed_members - existing_members).map { |i| [i, "+"] }
102
+ removed_from_group = (existing_members - proposed_members).map { |i| [i, "-"] }
103
+
104
+ # Filter out case-only differences. For example if "bob" is in existing and "BOB" is in proposed,
105
+ # we don't want to show this as a difference.
106
+ downcase_proposed_members = proposed_members.map { |m| m.downcase }
107
+ downcase_existing_members = existing_members.map { |m| m.downcase }
108
+ duplicated = downcase_proposed_members & downcase_existing_members
109
+ added_to_group.reject! { |m| duplicated.include?(m.first.downcase) }
110
+ removed_from_group.reject! { |m| duplicated.include?(m.first.downcase) }
111
+
112
+ # What's left is actual changes.
113
+ combined_group = (added_to_group + removed_from_group).sort_by { |i| i.first.downcase }
114
+ if combined_group.any?
115
+ Entitlements.logger.info "CHANGE #{identifier} in #{key}"
116
+ combined_group.each do |item, item_changetype|
117
+ Entitlements.logger.info ". #{item_changetype} #{item}"
118
+ end
119
+ end
120
+
121
+ if obj.existing.description != obj.updated.description && obj.ou_type == "ldap"
122
+ Entitlements.logger.info "METADATA CHANGE #{identifier} in #{key}"
123
+ Entitlements.logger.info "- Old description: #{obj.existing.description.inspect}"
124
+ Entitlements.logger.info "+ New description: #{obj.updated.description.inspect}"
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ # Get count of changes.
131
+ #
132
+ # Takes no arguments.
133
+ #
134
+ # Returns an Integer.
135
+ Contract C::None => Integer
136
+ def change_count
137
+ actions.size
138
+ end
139
+
140
+ # Stub methods
141
+ # :nocov:
142
+ def prefetch
143
+ # Can be left undefined
144
+ end
145
+
146
+ def validate
147
+ # Can be left undefined
148
+ end
149
+
150
+ def calculate
151
+ raise "Must be defined in child class"
152
+ end
153
+
154
+ def preapply
155
+ # Can be left undefined
156
+ end
157
+
158
+ def apply(action)
159
+ raise "Must be defined in child class"
160
+ end
161
+
162
+ def validate_config!(key, data)
163
+ # Can be left undefined (but really shouldn't)
164
+ end
165
+
166
+ private
167
+
168
+ attr_reader :config, :group_name, :logger
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Entitlements
6
+ class Backend
7
+ class BaseProvider
8
+ include ::Contracts::Core
9
+ C = ::Contracts
10
+
11
+ # Dry run of committing changes. Returns a list of users added or removed.
12
+ # Takes a group; looks up that same group in the appropriate backend.
13
+ #
14
+ # group - An Entitlements::Models::Group object.
15
+ # ignored_users - Optionally, a Set of lower-case Strings of users to ignore.
16
+ #
17
+ # Returns added / removed hash.
18
+ Contract Entitlements::Models::Group, C::Maybe[C::SetOf[String]] => Hash[added: C::SetOf[String], removed: C::SetOf[String]]
19
+ def diff(group, ignored_users = Set.new)
20
+ existing_group = read(group.cn.downcase)
21
+ return diff_existing_updated(existing_group, group, ignored_users)
22
+ end
23
+
24
+ # Dry run of committing changes. Returns a list of users added or removed.
25
+ # Takes an existing and an updated group object, avoiding a lookup in the backend.
26
+ #
27
+ # existing_group - An Entitlements::Models::Group object.
28
+ # group - An Entitlements::Models::Group object.
29
+ # ignored_users - Optionally, a Set of lower-case Strings of users to ignore.
30
+ Contract Entitlements::Models::Group, Entitlements::Models::Group, C::Maybe[C::SetOf[String]] => Hash[added: C::SetOf[String], removed: C::SetOf[String]]
31
+ def diff_existing_updated(existing_group, group, ignored_users = Set.new)
32
+ # The comparison needs to be done case-insensitive because some backends (e.g. GitHub organizations or teams)
33
+ # may report members with different capitalization than is used in Entitlements. Keep track of correct capitalization
34
+ # of member names here so they can be applied later. Note that `group` (from Entitlements) overrides `existing_group`
35
+ # (from the backend).
36
+ member_with_correct_capitalization = existing_group.member_strings.map { |ms| [ms.downcase, ms] }.to_h
37
+ member_with_correct_capitalization.merge! group.member_strings.map { |ms| [ms.downcase, ms] }.to_h
38
+
39
+ existing_members = existing_group.member_strings.map { |u| u.downcase }
40
+ Entitlements::Util::Util.remove_uids(existing_members, ignored_users)
41
+
42
+ proposed_members = group.member_strings.map { |u| u.downcase }
43
+ Entitlements::Util::Util.remove_uids(proposed_members, ignored_users)
44
+
45
+ added_members = proposed_members - existing_members
46
+ removed_members = existing_members - proposed_members
47
+
48
+ {
49
+ added: Set.new(added_members.map { |ms| member_with_correct_capitalization[ms] }),
50
+ removed: Set.new(removed_members.map { |ms| member_with_correct_capitalization[ms] })
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Entitlements
4
+ class Backend
5
+ class Dummy
6
+ class Controller < Entitlements::Backend::BaseController
7
+ register
8
+
9
+ # :nocov:
10
+ include ::Contracts::Core
11
+ C = ::Contracts
12
+
13
+ # Pre-fetch the existing group membership in each OU.
14
+ #
15
+ # Takes no arguments.
16
+ #
17
+ # Returns nothing. (Populates cache.)
18
+ Contract C::None => C::Any
19
+ def prefetch
20
+ # This does nothing.
21
+ end
22
+
23
+ # Validation routines.
24
+ #
25
+ # Takes no arguments.
26
+ #
27
+ # Returns nothing. (Populates cache.)
28
+ Contract C::None => C::Any
29
+ def validate
30
+ # This does nothing.
31
+ end
32
+
33
+ # Get count of changes.
34
+ #
35
+ # Takes no arguments.
36
+ #
37
+ # Returns an Integer.
38
+ Contract C::None => Integer
39
+ def change_count
40
+ super
41
+ end
42
+
43
+ # Calculation routines.
44
+ #
45
+ # Takes no arguments.
46
+ #
47
+ # Returns nothing (populates @actions).
48
+ Contract C::None => C::Any
49
+ def calculate
50
+ # No point in calculating anything. Any references herein will be calculated automatically.
51
+ @actions = []
52
+ end
53
+
54
+ # Pre-apply routines.
55
+ #
56
+ # Takes no arguments.
57
+ #
58
+ # Returns nothing.
59
+ Contract C::None => C::Any
60
+ def preapply
61
+ # This does nothing.
62
+ end
63
+
64
+ # Apply changes.
65
+ #
66
+ # action - Action array.
67
+ #
68
+ # Returns nothing.
69
+ Contract Entitlements::Models::Action => C::Any
70
+ def apply(caction)
71
+ # This does nothing.
72
+ end
73
+
74
+ # Validate configuration options.
75
+ #
76
+ # key - String with the name of the group.
77
+ # data - Hash with the configuration data.
78
+ #
79
+ # Returns nothing.
80
+ Contract String, C::HashOf[String => C::Any] => nil
81
+ def validate_config!(key, data)
82
+ # Do nothing to validate. Pass whatever arguments you want, and this will just ignore them!
83
+ end
84
+
85
+ # :nocov:
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dummy/controller"