entitlements-app 0.1.6

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