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