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,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Entitlements
4
+ class Backend
5
+ class LDAP
6
+ class Controller < Entitlements::Backend::BaseController
7
+ register
8
+
9
+ include ::Contracts::Core
10
+ C = ::Contracts
11
+
12
+ # Constructor. Generic constructor that takes a hash of configuration options.
13
+ #
14
+ # group_name - Name of the corresponding group in the entitlements configuration file.
15
+ # config - Optionally, a Hash of configuration information (configuration is referenced if empty).
16
+ Contract String, C::Maybe[C::HashOf[String => C::Any]] => C::Any
17
+ def initialize(group_name, config = nil)
18
+ super
19
+
20
+ @ldap = Entitlements::Service::LDAP.new_with_cache(
21
+ addr: @config.fetch("ldap_uri"),
22
+ binddn: @config.fetch("ldap_binddn"),
23
+ bindpw: @config.fetch("ldap_bindpw"),
24
+ ca_file: @config.fetch("ldap_ca_file", ENV["LDAP_CACERT"]),
25
+ disable_ssl_verification: @config.fetch("ldap_disable_ssl_verification", false),
26
+ person_dn_format: @config.fetch("person_dn_format")
27
+ )
28
+ @provider = Entitlements::Backend::LDAP::Provider.new(ldap: @ldap)
29
+ end
30
+
31
+ # Pre-fetch the existing group membership in each OU.
32
+ #
33
+ # Takes no arguments.
34
+ #
35
+ # Returns nothing. (Populates cache.)
36
+ Contract C::None => C::Any
37
+ def prefetch
38
+ logger.debug "Pre-fetching group membership in #{group_name} (#{config['base']}) from LDAP"
39
+ provider.read_all(config["base"])
40
+ end
41
+
42
+ # Validation routines.
43
+ #
44
+ # Takes no arguments.
45
+ #
46
+ # Returns nothing.
47
+ Contract C::None => C::Any
48
+ def validate
49
+ return unless config["mirror"]
50
+ Entitlements::Util::Mirror.validate_mirror!(group_name)
51
+ end
52
+
53
+ # Get count of changes.
54
+ #
55
+ # Takes no arguments.
56
+ #
57
+ # Returns an Integer.
58
+ Contract C::None => Integer
59
+ def change_count
60
+ actions.size + (ou_needs_to_be_created? ? 1 : 0)
61
+ end
62
+
63
+ # Calculation routines.
64
+ #
65
+ # Takes no arguments.
66
+ #
67
+ # Returns nothing (populates @actions).
68
+ Contract C::None => C::Any
69
+ def calculate
70
+ if ou_needs_to_be_created?
71
+ logger.info "ADD #{config['base']}"
72
+ end
73
+
74
+ existing = provider.read_all(config["base"])
75
+ proposed = Entitlements::Data::Groups::Calculated.read_all(group_name, config)
76
+
77
+ # Calculate differences.
78
+ added = (proposed - existing)
79
+ .map { |i| Entitlements::Models::Action.new(i, nil, Entitlements::Data::Groups::Calculated.read(i), group_name) }
80
+ removed = (existing - proposed)
81
+ .map { |i| Entitlements::Models::Action.new(i, provider.read(i), nil, group_name) }
82
+ changed = (existing & proposed)
83
+ .reject { |i| provider.read(i).equals?(Entitlements::Data::Groups::Calculated.read(i)) }
84
+ .map { |i| Entitlements::Models::Action.new(i, provider.read(i), Entitlements::Data::Groups::Calculated.read(i), group_name) }
85
+
86
+ # Print the differences.
87
+ print_differences(key: group_name, added: added, removed: removed, changed: changed)
88
+
89
+ # Populate the actions
90
+ @actions = [added, removed, changed].flatten.compact
91
+ end
92
+
93
+ # Pre-apply routines. For the LDAP provider this creates the OU if it does not exist,
94
+ # and if "create_if_missing" has been set to true.
95
+ #
96
+ # Takes no arguments.
97
+ #
98
+ # Returns nothing.
99
+ Contract C::None => C::Any
100
+ def preapply
101
+ return unless ou_needs_to_be_created?
102
+
103
+ if ldap.upsert(dn: config["base"], attributes: {"objectClass" => "organizationalUnit"})
104
+ logger.debug "APPLY: Creating #{config['base']}"
105
+ else
106
+ logger.warn "DID NOT APPLY: Changes not needed to #{config['base']}"
107
+ end
108
+ end
109
+
110
+ # Apply changes.
111
+ #
112
+ # action - Action array.
113
+ #
114
+ # Returns nothing.
115
+ Contract Entitlements::Models::Action => C::Any
116
+ def apply(action)
117
+ if action.updated.nil?
118
+ logger.debug "APPLY: Deleting #{action.dn}"
119
+ ldap.delete(action.dn)
120
+ else
121
+ override = Entitlements::Util::Override.override_hash_from_plugin(action.config["plugin"], action.updated, ldap) || {}
122
+ if provider.upsert(action.updated, override)
123
+ logger.debug "APPLY: Upserting #{action.dn}"
124
+ else
125
+ logger.warn "DID NOT APPLY: Changes not needed to #{action.dn}"
126
+ logger.debug "Old: #{action.existing.inspect}"
127
+ logger.debug "New: #{action.updated.inspect}"
128
+ end
129
+ end
130
+ end
131
+
132
+ # Validate configuration options.
133
+ #
134
+ # key - String with the name of the group.
135
+ # data - Hash with the configuration data.
136
+ #
137
+ # Returns nothing.
138
+ Contract String, C::HashOf[String => C::Any] => nil
139
+ def validate_config!(key, data)
140
+ spec = COMMON_GROUP_CONFIG.merge({
141
+ "base" => { required: true, type: String },
142
+ "create_if_missing" => { required: false, type: [FalseClass, TrueClass]},
143
+ "ldap_binddn" => { required: true, type: String },
144
+ "ldap_bindpw" => { required: true, type: String },
145
+ "ldap_ca_file" => { required: false, type: String },
146
+ "disable_ssl_verification" => { required: false, type: [FalseClass, TrueClass] },
147
+ "ldap_uri" => { required: true, type: String },
148
+ "plugin" => { required: false, type: Hash },
149
+ "mirror" => { required: false, type: String },
150
+ "person_dn_format" => { required: true, type: String }
151
+ })
152
+ text = "Group #{key.inspect}"
153
+ Entitlements::Util::Util.validate_attr!(spec, data, text)
154
+ end
155
+
156
+ # ***********************************************************
157
+ # Private methods for this backend go below.
158
+ # ***********************************************************
159
+
160
+ # Determine if the OU needs to be created.
161
+ #
162
+ # Takes no arguments.
163
+ #
164
+ # Returns an Array of Strings (DNs).
165
+ Contract C::None => C::Bool
166
+ def ou_needs_to_be_created?
167
+ return false unless config["create_if_missing"]
168
+
169
+ @ou_needs_to_be_created ||= begin
170
+ if ldap.exists?(config["base"])
171
+ logger.debug "OU create_if_missing: #{config['base']} already exists"
172
+ :false
173
+ else
174
+ logger.debug "OU create_if_missing: #{config['base']} needs to be created"
175
+ :true
176
+ end
177
+ end
178
+
179
+ @ou_needs_to_be_created == :true
180
+ end
181
+
182
+ private
183
+
184
+ attr_reader :ldap, :provider
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Entitlements
4
+ class Backend
5
+ class LDAP
6
+ class Provider
7
+ include ::Contracts::Core
8
+ C = ::Contracts
9
+
10
+ # Constructor.
11
+ #
12
+ # ldap - Entitlements::Service::LDAP object
13
+ Contract C::KeywordArgs[
14
+ ldap: Entitlements::Service::LDAP,
15
+ ] => C::Any
16
+ def initialize(ldap:)
17
+ @ldap = ldap
18
+ @groups_in_ou_cache = {}
19
+
20
+ # Keep track of each LDAP group we have read so we do not end up re-reading
21
+ # certain referenced groups over and over again. This is at a program-wide level. If
22
+ # multiple backends have the same namespace this will probably break.
23
+ Entitlements.cache[:ldap_cache] ||= {}
24
+ end
25
+
26
+ # Read in a specific LDAP group and enumerate its members. Results are cached
27
+ # for future runs.
28
+ #
29
+ # dn - A String with the DN of the group to read
30
+ #
31
+ # Returns a Entitlements::Models::Group object.
32
+ Contract String => Entitlements::Models::Group
33
+ def read(dn)
34
+ Entitlements.cache[:ldap_cache][dn] ||= begin
35
+ Entitlements.logger.debug "Loading group #{dn}"
36
+
37
+ # Look up the group by its DN.
38
+ result = ldap.search(base: dn, scope: Net::LDAP::SearchScope_BaseObject)
39
+
40
+ # Ensure exactly one result found.
41
+ unless result.size == 1 && result.key?(dn)
42
+ raise Entitlements::Data::Groups::GroupNotFoundError, "No response from LDAP for dn=#{dn}"
43
+ end
44
+
45
+ Entitlements::Service::LDAP.entry_to_group(result[dn])
46
+ end
47
+ end
48
+
49
+ # Read in LDAP groups within the specified OU and enumerate their members.
50
+ # Results are cached for future runs so the read() method is faster.
51
+ #
52
+ # ou - A String with the base OU to search
53
+ #
54
+ # Returns a Set of Strings (DNs) of the groups in this OU.
55
+ Contract String => C::SetOf[String]
56
+ def read_all(ou)
57
+ @groups_in_ou_cache[ou] ||= begin
58
+ Entitlements.logger.debug "Loading all groups for #{ou}"
59
+
60
+ # Find all groups in the OU
61
+ raw = ldap.search(
62
+ base: ou,
63
+ filter: Net::LDAP::Filter.eq("cn", "*"),
64
+ scope: Net::LDAP::SearchScope_SingleLevel
65
+ )
66
+
67
+ # Construct a Set of DNs from the result, and also cache the content of the
68
+ # group to avoid a future lookup.
69
+ result = Set.new
70
+ raw.each do |dn, entry|
71
+ Entitlements.cache[:ldap_cache][dn] = Entitlements::Service::LDAP.entry_to_group(entry)
72
+ result.add dn
73
+ end
74
+
75
+ # Return that result
76
+ result
77
+ end
78
+ end
79
+
80
+ # Delete an LDAP group.
81
+ #
82
+ # dn - A String with the DN of the group to delete.
83
+ #
84
+ # Returns nothing.
85
+ Contract String => nil
86
+ def delete(dn)
87
+ return if ldap.delete(dn)
88
+ raise "Unable to delete LDAP group #{dn.inspect}!"
89
+ end
90
+
91
+ # Commit changes (upsert).
92
+ #
93
+ # group - An Entitlements::Models::Group object.
94
+ # override - An optional Hash of additional parameters that override defaults.
95
+ #
96
+ # Returns true if a change was made, false if no change was made.
97
+ Contract Entitlements::Models::Group, C::Maybe[C::HashOf[String => C::Any]] => C::Bool
98
+ def upsert(group, override = {})
99
+ members = group.member_strings.map { |ms| ldap.person_dn_format.gsub("%KEY%", ms) }
100
+
101
+ attributes = {
102
+ "uniqueMember" => members,
103
+ "description" => group.description || "",
104
+ "owner" => [ldap.binddn],
105
+ "objectClass" => ["groupOfUniqueNames"],
106
+ "cn" => group.cn
107
+ }.merge(override)
108
+ override.each { |key, val| attributes.delete(key) if val.nil? }
109
+
110
+ # LDAP schema does not allow empty groups but does allow a group to be a member of itself.
111
+ # This gets around having to commit a dummy user in case an LDAP group is empty.
112
+ if group.member_strings.empty?
113
+ attributes["uniqueMember"] = [group.dn]
114
+ end
115
+
116
+ result = ldap.upsert(dn: group.dn, attributes: attributes)
117
+ return result if result == true
118
+ return false if result.nil?
119
+ raise "Unable to upsert LDAP group #{group.dn.inspect}!"
120
+ end
121
+
122
+ private
123
+
124
+ attr_reader :ldap
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ldap/controller"
4
+ require_relative "ldap/provider"
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Entitlements
4
+ class Backend
5
+ class MemberOf
6
+ class Controller < Entitlements::Backend::BaseController
7
+ # Controller priority and registration
8
+ def self.priority
9
+ 20
10
+ end
11
+
12
+ register
13
+
14
+ include ::Contracts::Core
15
+ C = ::Contracts
16
+
17
+ # Constructor. Generic constructor that takes a hash of configuration options.
18
+ #
19
+ # group_name - Name of the corresponding group in the entitlements configuration file.
20
+ # config - Optionally, a Hash of configuration information (configuration is referenced if empty).
21
+ Contract String, C::Maybe[C::HashOf[String => C::Any]] => C::Any
22
+ def initialize(group_name, config = nil)
23
+ super
24
+
25
+ @ldap = Entitlements::Service::LDAP.new_with_cache(
26
+ addr: @config.fetch("ldap_uri"),
27
+ binddn: @config.fetch("ldap_binddn"),
28
+ bindpw: @config.fetch("ldap_bindpw"),
29
+ ca_file: @config.fetch("ldap_ca_file", ENV["LDAP_CACERT"]),
30
+ disable_ssl_verification: @config.fetch("ldap_disable_ssl_verification", false),
31
+ person_dn_format: @config.fetch("person_dn_format")
32
+ )
33
+ end
34
+
35
+ # Calculation routines.
36
+ #
37
+ # Takes no arguments.
38
+ #
39
+ # Returns nothing (populates @actions).
40
+ Contract C::None => C::Any
41
+ def calculate
42
+ logger.debug "Calculating memberOf attributes for configured groups"
43
+
44
+ # We need to update people attributes for each group that is calculated and tagged with an
45
+ # attribute that needs to be updated.
46
+ cleared = Set.new
47
+
48
+ relevant_groups = Entitlements::Data::Groups::Calculated.all_groups.select do |ou_key, _|
49
+ config["ou"].include?(ou_key)
50
+ end
51
+
52
+ unless relevant_groups.any?
53
+ raise "memberOf emulator found no OUs matching: #{config['ou'].join(', ')}"
54
+ end
55
+
56
+ attribute = config["memberof_attribute"]
57
+
58
+ relevant_groups.each do |ou_key, data|
59
+ if cleared.add?(attribute)
60
+ Entitlements.cache[:people_obj].read.each do |uid, _person|
61
+ Entitlements.cache[:people_obj].read(uid)[attribute] = []
62
+ end
63
+ end
64
+
65
+ data[:groups].each do |group_dn, group_data|
66
+ group_data.member_strings.each do |member|
67
+ Entitlements.cache[:people_obj].read(member).add(attribute, group_dn)
68
+ end
69
+ end
70
+ end
71
+
72
+ # Now to populate the actions we have to see which persons have changed attributes.
73
+ @actions = Entitlements.cache[:people_obj].read
74
+ .reject { |_uid, person| person.attribute_changes.empty? }
75
+ .map do |person_uid, person|
76
+ print_differences(person)
77
+
78
+ Entitlements::Models::Action.new(
79
+ person_uid,
80
+ :none, # Convention, since entitlements doesn't (yet) create people
81
+ person,
82
+ group_name
83
+ )
84
+ end
85
+ end
86
+
87
+ # Apply changes.
88
+ #
89
+ # action - Action array.
90
+ #
91
+ # Returns nothing.
92
+ Contract Entitlements::Models::Action => C::Any
93
+ def apply(action)
94
+ person = action.updated
95
+ changes = person.attribute_changes
96
+ changes.each do |attrib, val|
97
+ if val.nil?
98
+ logger.debug "APPLY: Delete #{attrib} from #{person.uid}"
99
+ else
100
+ logger.debug "APPLY: Upsert #{attrib} to #{person.uid}"
101
+ end
102
+ end
103
+
104
+ person_dn = ldap.person_dn_format.gsub("%KEY%", person.uid)
105
+ unless ldap.modify(person_dn, changes)
106
+ logger.warn "DID NOT APPLY: Changes to #{person.uid} failed!"
107
+ raise "LDAP modify error on #{person_dn}!"
108
+ end
109
+ end
110
+
111
+ # Print difference array. The difference printer is a bit different for people rather than
112
+ # groups, since groups really only had a couple attributes of interest but were spread across
113
+ # multiple OUs, whereas people exist in one OU but have multiple attributes of interest.
114
+ #
115
+ # person - An Entitlements::Models::Person object.
116
+ #
117
+ # Returns nothing (this just prints to logger).
118
+ Contract Entitlements::Models::Person => C::Any
119
+ def print_differences(person)
120
+ changes = person.attribute_changes
121
+ return if changes.empty?
122
+
123
+ plural = changes.size == 1 ? "" : "s"
124
+ logger.info "Person #{person.uid} attribute change#{plural}:"
125
+
126
+ changes.sort.to_h.each do |attrib, val|
127
+ orig = person.original(attrib)
128
+ if orig.nil?
129
+ # Added attribute
130
+ if val.is_a?(Array)
131
+ logger.info ". ADD attribute #{attrib}:"
132
+ val.each { |item| logger.info ". + #{item}" }
133
+ else
134
+ logger.info ". ADD attribute #{attrib}: #{val.inspect}"
135
+ end
136
+ elsif val.nil?
137
+ # Removed attribute
138
+ if orig.is_a?(Array)
139
+ word = orig.size == 1 ? "entry" : "entries"
140
+ logger.info ". REMOVE attribute #{attrib}: #{orig.size} #{word}"
141
+ else
142
+ logger.info ". REMOVE attribute #{attrib}: #{orig.inspect}"
143
+ end
144
+ else
145
+ # Modified attribute
146
+ logger.info ". MODIFY attribute #{attrib}:"
147
+ if val.is_a?(String) && orig.is_a?(String)
148
+ # Simple string change
149
+ logger.info ". - #{orig.inspect}"
150
+ logger.info ". + #{val.inspect}"
151
+ elsif val.is_a?(Array) && orig.is_a?(Array)
152
+ # Array difference
153
+ added = Set.new(val - orig)
154
+ removed = Set.new(orig - val)
155
+ combined = (added.to_a + removed.to_a)
156
+ combined.sort.each do |item|
157
+ sign = added.member?(item) ? "+" : "-"
158
+ logger.info ". #{sign} #{item.inspect}"
159
+ end
160
+ else
161
+ # Data type mismatch is unexpected, so don't try to handle every possible case.
162
+ # This should only happen if LDAP schema changes. Just dump out the data structures.
163
+ logger.info ". - (#{orig.class})"
164
+ logger.info ". + #{val.inspect}"
165
+ end
166
+ end
167
+ end
168
+
169
+ # Return nil to satisfy contract
170
+ nil
171
+ end
172
+
173
+ private
174
+
175
+ attr_reader :ldap
176
+
177
+ # Validate configuration options.
178
+ #
179
+ # key - String with the name of the group.
180
+ # data - Hash with the configuration data.
181
+ #
182
+ # Returns nothing.
183
+ # :nocov:
184
+ Contract String, C::HashOf[String => C::Any] => nil
185
+ def validate_config!(key, data)
186
+ spec = COMMON_GROUP_CONFIG.merge({
187
+ "ldap_binddn" => { required: true, type: String },
188
+ "ldap_bindpw" => { required: true, type: String },
189
+ "ldap_ca_file" => { required: false, type: String },
190
+ "ldap_uri" => { required: true, type: String },
191
+ "disable_ssl_verification" => { required: false, type: [FalseClass, TrueClass] },
192
+ "memberof_attribute" => { required: true, type: String },
193
+ "person_dn_format" => { required: true, type: String },
194
+ "ou" => { required: true, type: Array }
195
+ })
196
+ text = "Group #{key.inspect}"
197
+ Entitlements::Util::Util.validate_attr!(spec, data, text)
198
+ end
199
+ # :nocov:
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "member_of/controller"
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optimist"
4
+
5
+ module Entitlements
6
+ class Cli
7
+ include ::Contracts::Core
8
+ C = ::Contracts
9
+
10
+ # This is the entrypoint to the CLI.
11
+ #
12
+ # argv - An Array with arguments, defaults to ARGV
13
+ #
14
+ # Returns an Integer with the exit status.
15
+ # :nocov:
16
+ Contract C::ArrayOf[C::Or[String, C::Bool, C::Num]] => Integer
17
+ def self.run
18
+ # Establish the logger object. Debugging is enabled or disabled based on options.
19
+ logger = Logger.new(STDERR)
20
+ logger.level = options[:debug] ? Logger::DEBUG : Logger::INFO
21
+ Entitlements.set_logger(logger)
22
+
23
+ # Set up configuration file.
24
+ Entitlements.config_file = options[:"config-file"]
25
+ Entitlements.validate_configuration_file!
26
+
27
+ # Support predictive updates based on the environment, but allow the --full option to
28
+ # suppress setting and using it.
29
+ if ENV["ENTITLEMENTS_PREDICTIVE_STATE_DIR"] && !@options[:full]
30
+ Entitlements::Data::Groups::Cached.load_caches(ENV["ENTITLEMENTS_PREDICTIVE_STATE_DIR"])
31
+ end
32
+
33
+ # Calculate differences.
34
+ actions = Entitlements.calculate
35
+
36
+ # No-op mode exits here.
37
+ if @options[:noop]
38
+ logger.info "No-op mode is set. Would make #{Entitlements.cache[:change_count]} change(s)."
39
+ return 0
40
+ end
41
+
42
+ # No changes?
43
+ if actions.empty?
44
+ logger.info "No changes to be made. You're all set, friend! :sparkles:"
45
+ return 0
46
+ end
47
+
48
+ # Execute the changes. This raises if it fails to apply a change or if auditors fail.
49
+ Entitlements.execute(actions: actions)
50
+
51
+ # Done.
52
+ logger.info "Successfully applied #{Entitlements.cache[:change_count]} change(s)!"
53
+ 0
54
+ end
55
+ # :nocov:
56
+
57
+ # Method access to options. `attr_reader` doesn't work here since it's a class variable.
58
+ #
59
+ # Takes no arguments.
60
+ #
61
+ # Returns a Hash.
62
+ # :nocov:
63
+ Contract C::None => C::HashOf[Symbol => C::Any]
64
+ def self.options
65
+ @options ||= begin
66
+ o = initialize_options
67
+ validate_options!(o)
68
+ o
69
+ end
70
+ end
71
+ # :nocov:
72
+
73
+ # Parse the options given by ARGV and return a hash.
74
+ #
75
+ # Takes no arguments.
76
+ #
77
+ # Returns a Hash.
78
+ # :nocov:
79
+ Contract C::None => C::HashOf[Symbol => C::Any]
80
+ def self.initialize_options
81
+ Optimist.options do
82
+ banner <<-EOS
83
+ Configure authentication providers to look like the configuration declared in the files.
84
+
85
+ Usage:
86
+
87
+ $ deploy-entitlements --dir /path/to/configurations
88
+ .
89
+ EOS
90
+ opt :"config-file",
91
+ "Configuration file for application",
92
+ type: :string,
93
+ default: File.expand_path("../../config/entitlements/config.yaml", File.dirname(__FILE__))
94
+ opt :noop,
95
+ "no-op mode (do not actually apply configurations)",
96
+ type: :boolean, default: false
97
+ opt :full,
98
+ "full deployment (skip predictive updates, if configured)",
99
+ type: :boolean, default: false
100
+ opt :debug,
101
+ "debug messages enabled",
102
+ type: :boolean, default: false
103
+ end
104
+ end
105
+ # :nocov:
106
+
107
+ # Validate command line options.
108
+ #
109
+ # options - Hash of options.
110
+ #
111
+ # Returns nothing.
112
+ # :nocov:
113
+ Contract C::HashOf[Symbol => C::Any] => nil
114
+ def self.validate_options!(options)
115
+ unless options[:"config-file"].is_a?(String) && File.file?(options[:"config-file"])
116
+ raise ArgumentError, "Expected --config-file to be a valid file!"
117
+ end
118
+ end
119
+ # :nocov:
120
+ end
121
+ end