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,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/ldap"
|
4
|
+
|
5
|
+
module Entitlements
|
6
|
+
class Data
|
7
|
+
class People
|
8
|
+
class LDAP
|
9
|
+
include ::Contracts::Core
|
10
|
+
C = ::Contracts
|
11
|
+
|
12
|
+
# Default attributes
|
13
|
+
PEOPLE_ATTRIBUTES = %w[cn]
|
14
|
+
UID_ATTRIBUTE = "uid"
|
15
|
+
|
16
|
+
# Parameters
|
17
|
+
PARAMETERS = {
|
18
|
+
"base" => { required: true, type: String },
|
19
|
+
"ldap_binddn" => { required: true, type: String },
|
20
|
+
"ldap_bindpw" => { required: true, type: String },
|
21
|
+
"ldap_uri" => { required: true, type: String },
|
22
|
+
"ldap_ca_file" => { required: false, type: String },
|
23
|
+
"person_dn_format" => { required: true, type: String },
|
24
|
+
"disable_ssl_verification" => { required: false, type: [FalseClass, TrueClass] },
|
25
|
+
"additional_attributes" => { required: false, type: Array },
|
26
|
+
"uid_attribute" => { required: false, type: String }
|
27
|
+
}
|
28
|
+
|
29
|
+
# Fingerprint for the object based on unique parameters from the group configuration. If the fingerprint
|
30
|
+
# matches the same object should be re-used. This will raise an error if insufficient configuration is
|
31
|
+
# given.
|
32
|
+
#
|
33
|
+
# config - Hash of configuration values as may be found in the Entitlements configuration file.
|
34
|
+
#
|
35
|
+
# Returns a String with the "fingerprint" for this configuration.
|
36
|
+
Contract C::HashOf[String => C::Any] => String
|
37
|
+
def self.fingerprint(config)
|
38
|
+
PARAMETERS.keys.map { |key| config[key].inspect }.join("||")
|
39
|
+
end
|
40
|
+
|
41
|
+
# Construct this object based on parameters in a group configuration. This is the direct translation
|
42
|
+
# between the Entitlements configuration file (which is always a Hash with configuration values) and
|
43
|
+
# the object constructed from this class (which can have whatever structure makes sense).
|
44
|
+
#
|
45
|
+
# config - Hash of configuration values as may be found in the Entitlements configuration file.
|
46
|
+
#
|
47
|
+
# Returns Entitlements::Data::People::LDAP object.
|
48
|
+
# :nocov:
|
49
|
+
Contract C::HashOf[String => C::Any] => Entitlements::Data::People::LDAP
|
50
|
+
def self.new_from_config(config)
|
51
|
+
new(
|
52
|
+
ldap: Entitlements::Service::LDAP.new_with_cache(
|
53
|
+
addr: config.fetch("ldap_uri"),
|
54
|
+
binddn: config.fetch("ldap_binddn"),
|
55
|
+
bindpw: config.fetch("ldap_bindpw"),
|
56
|
+
ca_file: config.fetch("ldap_ca_file", ENV["LDAP_CACERT"]),
|
57
|
+
disable_ssl_verification: config.fetch("ldap_disable_ssl_verification", false),
|
58
|
+
person_dn_format: config.fetch("person_dn_format")
|
59
|
+
),
|
60
|
+
people_ou: config.fetch("base"),
|
61
|
+
uid_attr: config.fetch("uid_attribute", UID_ATTRIBUTE),
|
62
|
+
people_attr: config.fetch("additional_attributes", PEOPLE_ATTRIBUTES)
|
63
|
+
)
|
64
|
+
end
|
65
|
+
# :nocov:
|
66
|
+
|
67
|
+
# Validate configuration options.
|
68
|
+
#
|
69
|
+
# key - String with the name of the data source.
|
70
|
+
# config - Hash with the configuration data.
|
71
|
+
#
|
72
|
+
# Returns nothing.
|
73
|
+
# :nocov:
|
74
|
+
Contract String, C::HashOf[String => C::Any] => nil
|
75
|
+
def self.validate_config!(key, config)
|
76
|
+
text = "LDAP people configuration for data source #{key.inspect}"
|
77
|
+
Entitlements::Util::Util.validate_attr!(PARAMETERS, config, text)
|
78
|
+
end
|
79
|
+
# :nocov:
|
80
|
+
|
81
|
+
# Constructor.
|
82
|
+
#
|
83
|
+
# ldap - Entitlements::Service::LDAP object
|
84
|
+
# people_ou - String containing the OU in which people reside
|
85
|
+
# dn_format - How to translate a UID to a DN (e.g. uid=%KEY%,ou=People,dc=kittens,dc=net)
|
86
|
+
# uid_attr - Optional String with attribute name for the user ID (default: uid)
|
87
|
+
# people_attr - Optional Array of Strings attributes of people that should be fetched from LDAP
|
88
|
+
Contract C::KeywordArgs[
|
89
|
+
ldap: Entitlements::Service::LDAP,
|
90
|
+
people_ou: String,
|
91
|
+
uid_attr: C::Maybe[String],
|
92
|
+
people_attr: C::Maybe[C::ArrayOf[String]]
|
93
|
+
] => C::Any
|
94
|
+
def initialize(ldap:, people_ou:, uid_attr: UID_ATTRIBUTE, people_attr: PEOPLE_ATTRIBUTES)
|
95
|
+
@ldap = ldap
|
96
|
+
@people_ou = people_ou
|
97
|
+
@uid_attr = uid_attr
|
98
|
+
@people_attr = people_attr
|
99
|
+
end
|
100
|
+
|
101
|
+
# Read in the people from LDAP. Cache result for later access.
|
102
|
+
#
|
103
|
+
# uid - Optionally a uid to return. If not specified, returns the entire hash.
|
104
|
+
#
|
105
|
+
# Returns Hash of { uid => Entitlements::Models::Person } or one Entitlements::Models::Person.
|
106
|
+
Contract C::Maybe[String] => C::Or[C::HashOf[String => Entitlements::Models::Person], Entitlements::Models::Person]
|
107
|
+
def read(uid = nil)
|
108
|
+
@people ||= begin
|
109
|
+
Entitlements.logger.debug "Loading people from LDAP"
|
110
|
+
ldap.search(base: people_ou, filter: Net::LDAP::Filter.eq(uid_attr, "*"), attrs: people_attr.sort)
|
111
|
+
.map { |person_dn, entry| [Entitlements::Util::Util.first_attr(person_dn).downcase, entry_to_person(entry)] }
|
112
|
+
.to_h
|
113
|
+
end
|
114
|
+
|
115
|
+
return @people if uid.nil?
|
116
|
+
return @people[uid.downcase] if @people[uid.downcase]
|
117
|
+
raise Entitlements::Data::People::NoSuchPersonError, "read(#{uid.inspect}) matched no known person"
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
attr_reader :ldap, :people_ou, :uid_attr, :people_attr
|
123
|
+
|
124
|
+
# Construct an Entitlements::Models::Person from a Net::LDAP::Entry
|
125
|
+
#
|
126
|
+
# entry - The Net::LDAP::Entry
|
127
|
+
#
|
128
|
+
# Returns an Entitlements::Models::Person object.
|
129
|
+
Contract Net::LDAP::Entry => Entitlements::Models::Person
|
130
|
+
def entry_to_person(entry)
|
131
|
+
attributes = people_attr
|
132
|
+
.map { |k| [k.to_s, entry[k.to_sym]] }
|
133
|
+
.to_h
|
134
|
+
Entitlements::Models::Person.new(
|
135
|
+
uid: Entitlements::Util::Util.first_attr(entry.dn),
|
136
|
+
attributes: attributes
|
137
|
+
)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
module Entitlements
|
6
|
+
class Data
|
7
|
+
class People
|
8
|
+
class YAML
|
9
|
+
include ::Contracts::Core
|
10
|
+
C = ::Contracts
|
11
|
+
|
12
|
+
# Parameters
|
13
|
+
PARAMETERS = {
|
14
|
+
"filename" => { required: true, type: String }
|
15
|
+
}
|
16
|
+
|
17
|
+
# Fingerprint for the object based on unique parameters from the group configuration. If the fingerprint
|
18
|
+
# matches the same object should be re-used. This will raise an error if insufficient configuration is
|
19
|
+
# given.
|
20
|
+
#
|
21
|
+
# config - Hash of configuration values as may be found in the Entitlements configuration file.
|
22
|
+
#
|
23
|
+
# Returns a String with the "fingerprint" for this configuration.
|
24
|
+
Contract C::HashOf[String => C::Any] => String
|
25
|
+
def self.fingerprint(config)
|
26
|
+
PARAMETERS.keys.map { |key| config[key].inspect }.join("||")
|
27
|
+
end
|
28
|
+
|
29
|
+
# Construct this object based on parameters in a group configuration. This is the direct translation
|
30
|
+
# between the Entitlements configuration file (which is always a Hash with configuration values) and
|
31
|
+
# the object constructed from this class (which can have whatever structure makes sense).
|
32
|
+
#
|
33
|
+
# config - Hash of configuration values as may be found in the Entitlements configuration file.
|
34
|
+
#
|
35
|
+
# Returns Entitlements::Data::People::YAML object.
|
36
|
+
# :nocov:
|
37
|
+
Contract C::HashOf[String => C::Any] => Entitlements::Data::People::YAML
|
38
|
+
def self.new_from_config(config)
|
39
|
+
new(filename: config.fetch("filename"))
|
40
|
+
end
|
41
|
+
# :nocov:
|
42
|
+
|
43
|
+
# Validate configuration options.
|
44
|
+
#
|
45
|
+
# key - String with the name of the data source.
|
46
|
+
# config - Hash with the configuration data.
|
47
|
+
#
|
48
|
+
# Returns nothing.
|
49
|
+
# :nocov:
|
50
|
+
Contract String, C::HashOf[String => C::Any] => nil
|
51
|
+
def self.validate_config!(key, config)
|
52
|
+
text = "YAML people configuration for data source #{key.inspect}"
|
53
|
+
Entitlements::Util::Util.validate_attr!(PARAMETERS, config, text)
|
54
|
+
end
|
55
|
+
# :nocov:
|
56
|
+
|
57
|
+
# Constructor.
|
58
|
+
#
|
59
|
+
# filename - String with the filename to read.
|
60
|
+
# people - Optionally, Hash of { uid => Entitlements::Models::Person }
|
61
|
+
Contract C::KeywordArgs[
|
62
|
+
filename: String,
|
63
|
+
people: C::Maybe[C::HashOf[String => Entitlements::Models::Person]]
|
64
|
+
] => C::Any
|
65
|
+
def initialize(filename:, people: nil)
|
66
|
+
@filename = filename
|
67
|
+
@people = people
|
68
|
+
@people_downcase = nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# Read in the people from a file. Cache result for later access.
|
72
|
+
#
|
73
|
+
# uid - Optionally a uid to return. If not specified, returns the entire hash.
|
74
|
+
#
|
75
|
+
# Returns Hash of { uid => Entitlements::Models::Person } or one Entitlements::Models::Person.
|
76
|
+
Contract C::Maybe[String] => C::Or[Entitlements::Models::Person, C::HashOf[String => Entitlements::Models::Person]]
|
77
|
+
def read(uid = nil)
|
78
|
+
@people ||= begin
|
79
|
+
Entitlements.logger.debug "Loading people from #{filename.inspect}"
|
80
|
+
raw_person_data = ::YAML.load(File.read(filename)).to_h
|
81
|
+
raw_person_data.map do |id, data|
|
82
|
+
[id, Entitlements::Models::Person.new(uid: id, attributes: data)]
|
83
|
+
end.to_h
|
84
|
+
end
|
85
|
+
return @people if uid.nil?
|
86
|
+
|
87
|
+
# Requested a specific user ID
|
88
|
+
@people_downcase ||= @people.map { |person_uid, data| [person_uid.downcase, person_uid] }.to_h
|
89
|
+
unless @people_downcase.key?(uid.downcase)
|
90
|
+
raise Entitlements::Data::People::NoSuchPersonError, "read(#{uid.inspect}) matched no known person"
|
91
|
+
end
|
92
|
+
|
93
|
+
@people[@people_downcase[uid.downcase]]
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
attr_reader :filename
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "people/combined"
|
4
|
+
require_relative "people/dummy"
|
5
|
+
require_relative "people/ldap"
|
6
|
+
require_relative "people/yaml"
|
7
|
+
|
8
|
+
module Entitlements
|
9
|
+
class Data
|
10
|
+
class People
|
11
|
+
class NoSuchPersonError < RuntimeError; end
|
12
|
+
|
13
|
+
include ::Contracts::Core
|
14
|
+
C = ::Contracts
|
15
|
+
|
16
|
+
PEOPLE_CLASSES = {
|
17
|
+
"combined" => Entitlements::Data::People::Combined,
|
18
|
+
"dummy" => Entitlements::Data::People::Dummy,
|
19
|
+
"ldap" => Entitlements::Data::People::LDAP,
|
20
|
+
"yaml" => Entitlements::Data::People::YAML
|
21
|
+
}
|
22
|
+
|
23
|
+
# Gets the class for the specified configuration. Basically this is a wrapper around PEOPLE_CLASSES
|
24
|
+
# with friendly error messages if a configuration is insufficient to select the class.
|
25
|
+
#
|
26
|
+
# config - Hash of configuration values as may be found in the Entitlements configuration file.
|
27
|
+
#
|
28
|
+
# Returns Entitlements::Data::People class.
|
29
|
+
Contract C::HashOf[String => C::Any] => Class
|
30
|
+
def self.class_for_config(config)
|
31
|
+
unless config.key?("type")
|
32
|
+
raise ArgumentError, "'type' is undefined in: #{config.inspect}"
|
33
|
+
end
|
34
|
+
|
35
|
+
unless Entitlements::Data::People::PEOPLE_CLASSES.key?(config["type"])
|
36
|
+
raise ArgumentError, "'type' #{config['type'].inspect} is invalid!"
|
37
|
+
end
|
38
|
+
|
39
|
+
Entitlements::Data::People::PEOPLE_CLASSES.fetch(config["type"])
|
40
|
+
end
|
41
|
+
|
42
|
+
# Constructor to build an object from the configuration file. Given a key in the `groups`
|
43
|
+
# section, check the `people` key for one or more data sources for people records. Construct
|
44
|
+
# the underlying object(s) as necessary while caching duplicate objects.
|
45
|
+
#
|
46
|
+
# config - Hash of configuration values as may be found in the Entitlements configuration file.
|
47
|
+
#
|
48
|
+
# Returns Entitlements::Data::People object backed by appropriate object(s).
|
49
|
+
Contract C::HashOf[String => C::Any] => C::Any
|
50
|
+
def self.new_from_config(config)
|
51
|
+
Entitlements.cache[:people_class] ||= {}
|
52
|
+
clazz = class_for_config(config)
|
53
|
+
fingerprint = clazz.fingerprint(config.fetch("config"))
|
54
|
+
Entitlements.cache[:people_class][fingerprint] ||= clazz.new_from_config(config.fetch("config"))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Inherited methods for extras in this directory (or in other directories).
|
4
|
+
|
5
|
+
module Entitlements
|
6
|
+
module Extras
|
7
|
+
class Base
|
8
|
+
include ::Contracts::Core
|
9
|
+
C = ::Contracts
|
10
|
+
|
11
|
+
# Retrieve the configuration for this extra from the Entitlements configuration
|
12
|
+
# file. Returns the hash of configuration if found, or an empty hash in all other
|
13
|
+
# cases.
|
14
|
+
#
|
15
|
+
# Takes no arguments.
|
16
|
+
#
|
17
|
+
# Returns a Hash.
|
18
|
+
Contract C::None => C::HashOf[String => C::Any]
|
19
|
+
def self.config
|
20
|
+
@extra_config ||= begin
|
21
|
+
# classname is something like "Entitlements::Extras::MyExtraClassName::Base" - want to pull
|
22
|
+
# out the "MyExtraClassName" from this string.
|
23
|
+
classname = self.to_s.split("::")[-2]
|
24
|
+
decamelized_class = Entitlements::Util::Util.decamelize(classname)
|
25
|
+
cfg = Entitlements.config.fetch("extras", {}).fetch(decamelized_class, nil)
|
26
|
+
cfg.is_a?(Hash) ? cfg : {}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# This is intended for unit tests to reset class variables.
|
31
|
+
#
|
32
|
+
# Takes no arguments.
|
33
|
+
#
|
34
|
+
# Returns nothing.
|
35
|
+
def self.reset!
|
36
|
+
@extra_config = nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../base"
|
4
|
+
|
5
|
+
module Entitlements
|
6
|
+
module Extras
|
7
|
+
class LDAPGroup
|
8
|
+
class Base < Entitlements::Extras::Base
|
9
|
+
def self.init
|
10
|
+
require_relative "filters/member_of_ldap_group"
|
11
|
+
require_relative "rules/ldap_group"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.rules
|
15
|
+
%w[ldap_group]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Filter class to remove members of a particular LDAP group.
|
4
|
+
|
5
|
+
module Entitlements
|
6
|
+
module Extras
|
7
|
+
class LDAPGroup
|
8
|
+
class Filters
|
9
|
+
class MemberOfLDAPGroup < Entitlements::Data::Groups::Calculated::Filters::Base
|
10
|
+
include ::Contracts::Core
|
11
|
+
C = ::Contracts
|
12
|
+
|
13
|
+
# Determine if the member is filtered as per this definition. Return true if the member
|
14
|
+
# is to be filtered out, false if the member does not match the filter.
|
15
|
+
#
|
16
|
+
# member - Entitlements::Models::Person object
|
17
|
+
#
|
18
|
+
# Returns true if the person is to be filtered out, false otherwise.
|
19
|
+
Contract Entitlements::Models::Person => C::Bool
|
20
|
+
def filtered?(member)
|
21
|
+
return false if filter == :all
|
22
|
+
return false unless member_of_ldap_group?(member, config.fetch("ldap_group"))
|
23
|
+
return true if filter == :none
|
24
|
+
!member_of_filter?(member)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Helper method: Determine if the person is a member of an LDAP group that exists in
|
28
|
+
# the directory but is not managed by entitlements.
|
29
|
+
#
|
30
|
+
# member - Entitlements::Models::Person object
|
31
|
+
# group_dn - LDAP distinguished name of the group
|
32
|
+
#
|
33
|
+
# Returns true if a member of the group, false otherwise.
|
34
|
+
Contract Entitlements::Models::Person, String => C::Bool
|
35
|
+
def member_of_ldap_group?(member, group_dn)
|
36
|
+
Entitlements.cache[:member_of_ldap_group] ||= {}
|
37
|
+
Entitlements.cache[:member_of_ldap_group][group_dn] ||= begin
|
38
|
+
member_set = Entitlements::Extras::LDAPGroup::Rules::LDAPGroup.matches(value: group_dn)
|
39
|
+
member_set.map { |person| person.uid.downcase }
|
40
|
+
rescue Entitlements::Data::Groups::GroupNotFoundError
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
|
44
|
+
Entitlements.cache[:member_of_ldap_group][group_dn].include?(member.uid.downcase)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Is someone in an LDAP group?
|
3
|
+
|
4
|
+
module Entitlements
|
5
|
+
module Extras
|
6
|
+
class LDAPGroup
|
7
|
+
class Rules
|
8
|
+
class LDAPGroup < Entitlements::Data::Groups::Calculated::Rules::Base
|
9
|
+
include ::Contracts::Core
|
10
|
+
C = ::Contracts
|
11
|
+
|
12
|
+
# Interface method: Get a Set[Entitlements::Models::Person] matching this condition.
|
13
|
+
#
|
14
|
+
# value - The value to match.
|
15
|
+
# filename - Name of the file resulting in this rule being called
|
16
|
+
# options - Optional hash of additional method-specific options
|
17
|
+
#
|
18
|
+
# Returns a Set[Entitlements::Models::Person].
|
19
|
+
Contract C::KeywordArgs[
|
20
|
+
value: String,
|
21
|
+
filename: C::Maybe[String],
|
22
|
+
options: C::Optional[C::HashOf[Symbol => C::Any]]
|
23
|
+
] => C::SetOf[Entitlements::Models::Person]
|
24
|
+
def self.matches(value:, filename: nil, options: {})
|
25
|
+
Entitlements.cache[:ldap_cache] ||= {}
|
26
|
+
Entitlements.cache[:ldap_cache][value] ||= begin
|
27
|
+
entry = ldap.read(value)
|
28
|
+
unless entry
|
29
|
+
message = if filename
|
30
|
+
"Failed to read ldap_group = #{value} (referenced in #{filename})"
|
31
|
+
else
|
32
|
+
# :nocov:
|
33
|
+
"Failed to read ldap_group = #{value}"
|
34
|
+
# :nocov:
|
35
|
+
end
|
36
|
+
raise Entitlements::Data::Groups::GroupNotFoundError, message
|
37
|
+
end
|
38
|
+
Entitlements::Service::LDAP.entry_to_group(entry)
|
39
|
+
end
|
40
|
+
Entitlements.cache[:ldap_cache][value].members(people_obj: Entitlements.cache[:people_obj])
|
41
|
+
end
|
42
|
+
|
43
|
+
# Object to communicate with an LDAP backend.
|
44
|
+
#
|
45
|
+
# Takes no arguments.
|
46
|
+
#
|
47
|
+
# Returns a Entitlements::Service::LDAP object.
|
48
|
+
# :nocov:
|
49
|
+
Contract C::None => Entitlements::Service::LDAP
|
50
|
+
def self.ldap
|
51
|
+
@ldap ||= begin
|
52
|
+
config = Entitlements::Extras::LDAPGroup::Base.config
|
53
|
+
opts = {
|
54
|
+
addr: config.fetch("ldap_uri"),
|
55
|
+
binddn: config.fetch("ldap_binddn"),
|
56
|
+
bindpw: config.fetch("ldap_bindpw"),
|
57
|
+
ca_file: config.fetch("ldap_ca_file", ENV["LDAP_CACERT"]),
|
58
|
+
person_dn_format: config.fetch("person_dn_format")
|
59
|
+
}
|
60
|
+
opts[:disable_ssl_verification] = true if config.fetch("disable_ssl_verification", false)
|
61
|
+
Entitlements::Service::LDAP.new_with_cache(opts)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
# :nocov:
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../base"
|
4
|
+
require "yaml"
|
5
|
+
|
6
|
+
module Entitlements
|
7
|
+
module Extras
|
8
|
+
class Orgchart
|
9
|
+
class Base < Entitlements::Extras::Base
|
10
|
+
def self.init
|
11
|
+
require_relative "logic"
|
12
|
+
require_relative "person_methods"
|
13
|
+
require_relative "rules/direct_report"
|
14
|
+
require_relative "rules/management"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.rules
|
18
|
+
%w[direct_report management]
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.person_methods
|
22
|
+
%w[manager]
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.reset!
|
26
|
+
super
|
27
|
+
Entitlements::Extras::Orgchart::PersonMethods.reset!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Helper functions to implement our own business logic.
|
3
|
+
|
4
|
+
module Entitlements
|
5
|
+
module Extras
|
6
|
+
class Orgchart
|
7
|
+
class Logic
|
8
|
+
include ::Contracts::Core
|
9
|
+
C = ::Contracts
|
10
|
+
|
11
|
+
# Constructor for the people logic engine. Pass in the indexed hash of people in the organization
|
12
|
+
# and this will handle indexing, caching, and reporting relationship logic.
|
13
|
+
#
|
14
|
+
# people_hash - A Hash of { dn => Entitlements::Models::Person LDAP Object }
|
15
|
+
Contract C::KeywordArgs[
|
16
|
+
people: C::HashOf[String => Entitlements::Models::Person],
|
17
|
+
] => C::Any
|
18
|
+
def initialize(people:)
|
19
|
+
@people_hash = people.map { |uid, person| [uid.downcase, person] }.to_h
|
20
|
+
@direct_reports_cache = nil
|
21
|
+
@all_reports_cache = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Calculate the direct reports of a given person, returning a Set of all of the people who report to
|
25
|
+
# that person. Does NOT return the manager themselves as part of the result set. Returns an empty set
|
26
|
+
# if the person has nobody directly reporting to them.
|
27
|
+
#
|
28
|
+
# manager - Entitlements::Models::Person who is the manager or higher
|
29
|
+
#
|
30
|
+
# Returns a Set of Entitlements::Models::Person's.
|
31
|
+
Contract Entitlements::Models::Person => C::SetOf[Entitlements::Models::Person]
|
32
|
+
def direct_reports(manager)
|
33
|
+
manager_uid = manager.uid.downcase
|
34
|
+
direct_reports_cache.key?(manager_uid) ? direct_reports_cache[manager_uid] : Set.new
|
35
|
+
end
|
36
|
+
|
37
|
+
# Calculate the all reports of a given person, returning a Set of all of the people who report to
|
38
|
+
# that person directly or indirectly. Does NOT return the manager themselves as part of the result
|
39
|
+
# set. Returns an empty set if the person has nobody reporting to them.
|
40
|
+
#
|
41
|
+
# manager - Entitlements::Models::Person who is the manager or higher
|
42
|
+
#
|
43
|
+
# Returns a Set of LDAP object Entitlements::Models::Persons.
|
44
|
+
Contract Entitlements::Models::Person => C::SetOf[Entitlements::Models::Person]
|
45
|
+
def all_reports(manager)
|
46
|
+
manager_uid = manager.uid.downcase
|
47
|
+
all_reports_cache.key?(manager_uid) ? all_reports_cache[manager_uid] : Set.new
|
48
|
+
end
|
49
|
+
|
50
|
+
# Calculate the management chain for the person, returning a Set of all managers in that chain all
|
51
|
+
# the way to the top of the tree.
|
52
|
+
#
|
53
|
+
# person - Entitlements::Models::Person object
|
54
|
+
#
|
55
|
+
# Returns a Set of LDAP object openstructs.
|
56
|
+
Contract Entitlements::Models::Person => C::SetOf[Entitlements::Models::Person]
|
57
|
+
def management_chain(person)
|
58
|
+
person_uid = person.uid.downcase
|
59
|
+
|
60
|
+
@management_chain_cache ||= {}
|
61
|
+
return @management_chain_cache[person_uid] if @management_chain_cache[person_uid].is_a?(Set)
|
62
|
+
|
63
|
+
@management_chain_cache[person_uid] = Set.new
|
64
|
+
if person.manager && person.manager != person.uid
|
65
|
+
person_manager_uid = person.manager.downcase
|
66
|
+
unless @people_hash.key?(person_manager_uid)
|
67
|
+
# :nocov:
|
68
|
+
raise ArgumentError, "Manager #{person.manager.inspect} for person #{person.uid.inspect} does not exist!"
|
69
|
+
# :nocov:
|
70
|
+
end
|
71
|
+
# The recursive logic here will also define the management_chain_cache value for the manager,
|
72
|
+
# and when calculating that it will define the management_chain_cache value for that manager's manager,
|
73
|
+
# and so on. This ensures that each person's manager is only computed one time (when it's used) and
|
74
|
+
# subsequent lookups are all faster.
|
75
|
+
@management_chain_cache[person_uid].add @people_hash[person_manager_uid]
|
76
|
+
@management_chain_cache[person_uid].merge management_chain(@people_hash[person_manager_uid])
|
77
|
+
end
|
78
|
+
@management_chain_cache[person_uid]
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Iterate through the entire employee list and build up the lists of direct reports for each
|
84
|
+
# manager. Cache this so that the iteration only occurs one time.
|
85
|
+
#
|
86
|
+
# Returns a Hash of { "dn" => Set(Entitlements::Models::Person) }
|
87
|
+
Contract C::None => C::HashOf[String => C::SetOf[Entitlements::Models::Person]]
|
88
|
+
def direct_reports_cache
|
89
|
+
return @direct_reports_cache if @direct_reports_cache
|
90
|
+
|
91
|
+
Entitlements.logger.debug "Building #{self.class} direct_reports_cache"
|
92
|
+
|
93
|
+
@direct_reports_cache = {}
|
94
|
+
@people_hash.each do |uid, entry|
|
95
|
+
# If this person doesn't have a manager, then bail. (CEO)
|
96
|
+
next unless entry.manager && entry.manager != entry.uid
|
97
|
+
|
98
|
+
# Initialize their manager's list of direct reports if necessary, and add this person
|
99
|
+
# to that list.
|
100
|
+
person_manager_uid = entry.manager.downcase
|
101
|
+
@direct_reports_cache[person_manager_uid] ||= Set.new
|
102
|
+
@direct_reports_cache[person_manager_uid].add entry
|
103
|
+
end
|
104
|
+
|
105
|
+
Entitlements.logger.debug "Built #{self.class} direct_reports_cache"
|
106
|
+
|
107
|
+
@direct_reports_cache
|
108
|
+
end
|
109
|
+
|
110
|
+
# Iterate through the list of managers and build up the lists of direct and indirect reports for each
|
111
|
+
# manager. Cache this so that the iteration only occurs one time.
|
112
|
+
#
|
113
|
+
# Returns a Hash of { "dn" => Set(Entitlements::Models::Person) }
|
114
|
+
Contract C::None => C::HashOf[String => C::SetOf[Entitlements::Models::Person]]
|
115
|
+
def all_reports_cache
|
116
|
+
return @all_reports_cache if @all_reports_cache
|
117
|
+
|
118
|
+
Entitlements.logger.debug "Building #{self.class} all_reports_cache"
|
119
|
+
|
120
|
+
@all_reports_cache = {}
|
121
|
+
direct_reports_cache.keys.each do |manager_uid|
|
122
|
+
generate_recursive_reports(manager_uid.downcase)
|
123
|
+
end
|
124
|
+
|
125
|
+
Entitlements.logger.debug "Built #{self.class} all_reports_cache"
|
126
|
+
|
127
|
+
@all_reports_cache
|
128
|
+
end
|
129
|
+
|
130
|
+
# Recursive method to determine all reports (direct and indirect). Intended to be called
|
131
|
+
# from `reports` method, when @direct_reports_cache has been populated but @all_reports_cache
|
132
|
+
# has not yet been initialized. This will populate @all_reports_cache. This hits each manager
|
133
|
+
# only once - O(N).
|
134
|
+
#
|
135
|
+
# manager_uid - A String with the uid of the manager.
|
136
|
+
#
|
137
|
+
# No return value.
|
138
|
+
Contract String => nil
|
139
|
+
def generate_recursive_reports(manager_uid)
|
140
|
+
# If we've calculated it already, then just return early
|
141
|
+
return if @all_reports_cache.key?(manager_uid)
|
142
|
+
|
143
|
+
# We've visited, so start the entry for it.
|
144
|
+
@all_reports_cache[manager_uid] = Set.new
|
145
|
+
|
146
|
+
# Add each direct report, as well as that person's direct reports, and so on.
|
147
|
+
direct_reports_cache[manager_uid].each do |direct_report|
|
148
|
+
# The direct report is also an "all" report for their manager.
|
149
|
+
@all_reports_cache[manager_uid].add direct_report
|
150
|
+
direct_report_uid = direct_report.uid.downcase
|
151
|
+
|
152
|
+
# If the direct report has no reports of their own, no need to go any further.
|
153
|
+
next unless direct_reports_cache.key?(direct_report_uid)
|
154
|
+
|
155
|
+
# Call this method again, this time with the direct report as the key. If we've already
|
156
|
+
# calculated all descendant reports of this person, it'll return immediately. Otherwise
|
157
|
+
# it'll iterate through this logic again, ensuring that we only calculate descendants
|
158
|
+
# once for any given person.
|
159
|
+
generate_recursive_reports(direct_report_uid)
|
160
|
+
|
161
|
+
# Merge in the calculated result for all of this report's descendants.
|
162
|
+
@all_reports_cache[manager_uid].merge @all_reports_cache[direct_report_uid]
|
163
|
+
end
|
164
|
+
|
165
|
+
# Satisfy the contract for return value.
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|