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