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