entitlements-app 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/VERSION +1 -0
- data/bin/deploy-entitlements +18 -0
- data/lib/entitlements/auditor/base.rb +163 -0
- data/lib/entitlements/backend/base_controller.rb +171 -0
- data/lib/entitlements/backend/base_provider.rb +55 -0
- data/lib/entitlements/backend/dummy/controller.rb +89 -0
- data/lib/entitlements/backend/dummy.rb +3 -0
- data/lib/entitlements/backend/ldap/controller.rb +188 -0
- data/lib/entitlements/backend/ldap/provider.rb +128 -0
- data/lib/entitlements/backend/ldap.rb +4 -0
- data/lib/entitlements/backend/member_of/controller.rb +203 -0
- data/lib/entitlements/backend/member_of.rb +3 -0
- data/lib/entitlements/cli.rb +121 -0
- data/lib/entitlements/data/groups/cached.rb +120 -0
- data/lib/entitlements/data/groups/calculated/base.rb +478 -0
- data/lib/entitlements/data/groups/calculated/filters/base.rb +93 -0
- data/lib/entitlements/data/groups/calculated/filters/member_of_group.rb +32 -0
- data/lib/entitlements/data/groups/calculated/modifiers/base.rb +38 -0
- data/lib/entitlements/data/groups/calculated/modifiers/expiration.rb +56 -0
- data/lib/entitlements/data/groups/calculated/ruby.rb +137 -0
- data/lib/entitlements/data/groups/calculated/rules/base.rb +35 -0
- data/lib/entitlements/data/groups/calculated/rules/group.rb +129 -0
- data/lib/entitlements/data/groups/calculated/rules/username.rb +41 -0
- data/lib/entitlements/data/groups/calculated/text.rb +337 -0
- data/lib/entitlements/data/groups/calculated/yaml.rb +171 -0
- data/lib/entitlements/data/groups/calculated.rb +290 -0
- data/lib/entitlements/data/groups.rb +13 -0
- data/lib/entitlements/data/people/combined.rb +197 -0
- data/lib/entitlements/data/people/dummy.rb +71 -0
- data/lib/entitlements/data/people/ldap.rb +142 -0
- data/lib/entitlements/data/people/yaml.rb +102 -0
- data/lib/entitlements/data/people.rb +58 -0
- data/lib/entitlements/extras/base.rb +40 -0
- data/lib/entitlements/extras/ldap_group/base.rb +20 -0
- data/lib/entitlements/extras/ldap_group/filters/member_of_ldap_group.rb +50 -0
- data/lib/entitlements/extras/ldap_group/rules/ldap_group.rb +69 -0
- data/lib/entitlements/extras/orgchart/base.rb +32 -0
- data/lib/entitlements/extras/orgchart/logic.rb +171 -0
- data/lib/entitlements/extras/orgchart/person_methods.rb +55 -0
- data/lib/entitlements/extras/orgchart/rules/direct_report.rb +62 -0
- data/lib/entitlements/extras/orgchart/rules/management.rb +59 -0
- data/lib/entitlements/extras.rb +82 -0
- data/lib/entitlements/models/action.rb +82 -0
- data/lib/entitlements/models/group.rb +280 -0
- data/lib/entitlements/models/person.rb +149 -0
- data/lib/entitlements/plugins/dummy.rb +22 -0
- data/lib/entitlements/plugins/group_of_names.rb +28 -0
- data/lib/entitlements/plugins/posix_group.rb +46 -0
- data/lib/entitlements/plugins.rb +13 -0
- data/lib/entitlements/rule/base.rb +74 -0
- data/lib/entitlements/service/ldap.rb +405 -0
- data/lib/entitlements/util/mirror.rb +42 -0
- data/lib/entitlements/util/override.rb +64 -0
- data/lib/entitlements/util/util.rb +219 -0
- data/lib/entitlements.rb +606 -0
- 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,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,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
|