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,55 @@
|
|
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 PersonMethods < Entitlements::Extras::Orgchart::Base
|
10
|
+
include ::Contracts::Core
|
11
|
+
C = ::Contracts
|
12
|
+
|
13
|
+
# This method might be used within Entitlements::Models::Person to determine
|
14
|
+
# the manager for a given person based on the organization chart.
|
15
|
+
#
|
16
|
+
# person - Reference to Entitlements::Models::Person object calling this method
|
17
|
+
#
|
18
|
+
# Returns a String with the distinguished name of the person's manager.
|
19
|
+
Contract Entitlements::Models::Person => String
|
20
|
+
def self.manager(person)
|
21
|
+
# User to manager map is assumed to be stored in a YAML file wherein the key is the
|
22
|
+
# username and the value is a hash. The value contains a key "manager" with the username
|
23
|
+
# of the manager.
|
24
|
+
@user_to_manager_map ||= begin
|
25
|
+
unless config["manager_map_file"]
|
26
|
+
raise ArgumentError, "To use #{self}, `manager_map_file` must be defined in the configuration!"
|
27
|
+
end
|
28
|
+
|
29
|
+
manager_map_file = Entitlements::Util::Util.absolute_path(config["manager_map_file"])
|
30
|
+
|
31
|
+
unless File.file?(manager_map_file)
|
32
|
+
raise Errno::ENOENT, "The `manager_map_file` #{manager_map_file} does not exist!"
|
33
|
+
end
|
34
|
+
|
35
|
+
YAML.load(File.read(manager_map_file))
|
36
|
+
end
|
37
|
+
|
38
|
+
u = person.uid.downcase
|
39
|
+
unless @user_to_manager_map.key?(u)
|
40
|
+
raise "User #{u} is not included in manager map data!"
|
41
|
+
end
|
42
|
+
unless @user_to_manager_map[u]["manager"]
|
43
|
+
raise "User #{u} does not have a manager listed in manager map data!"
|
44
|
+
end
|
45
|
+
@user_to_manager_map[u]["manager"]
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.reset!
|
49
|
+
@user_to_manager_map = nil
|
50
|
+
@extra_config = nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Is someone in a direct report of the listed manager?
|
3
|
+
|
4
|
+
module Entitlements
|
5
|
+
module Extras
|
6
|
+
class Orgchart
|
7
|
+
class Rules
|
8
|
+
class DirectReport < 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
|
+
# Construct the manager's DN and object.
|
26
|
+
manager_uid = value.downcase
|
27
|
+
|
28
|
+
begin
|
29
|
+
manager = Entitlements.cache[:people_obj].read(manager_uid)
|
30
|
+
rescue Entitlements::Data::People::NoSuchPersonError
|
31
|
+
# This is fatal. If this defines a team by a manager who is no longer in LDAP then the
|
32
|
+
# entry needs to be corrected because those people have to report to someone...
|
33
|
+
Entitlements.logger.fatal "Manager #{manager_uid} does not exist for file #{filename}!"
|
34
|
+
raise "Manager #{manager_uid} does not exist!"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Call all_reports which will return the set of all direct and indirect reports.
|
38
|
+
# This is evaluated once per run of the program.
|
39
|
+
Entitlements.cache[:management_obj] ||= begin
|
40
|
+
Entitlements::Extras::Orgchart::Logic.new(people: Entitlements.cache.fetch(:people_obj).read)
|
41
|
+
end
|
42
|
+
|
43
|
+
# This is fatal. If this defines a manager who has nobody reporting to them, then they
|
44
|
+
# aren't a manager at all. The entry should be changed to "username" or otherwise the
|
45
|
+
# proper manager should be filled in.
|
46
|
+
if Entitlements.cache[:management_obj].direct_reports(manager).empty?
|
47
|
+
Entitlements.logger.fatal "Manager #{manager_uid} has no reports for file #{filename}!"
|
48
|
+
raise "Manager #{manager_uid} has no reports!"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Most of the time, people will expect "direct_report: xyz" to include xyz and anyone who
|
52
|
+
# reports to them. Technically, xyz doesn't report to themself, but we'll hack it in here
|
53
|
+
# because it's least surprise. If someone really wants "xyz's reports but not xyz" they
|
54
|
+
# can use the "not" in conjunction.
|
55
|
+
result = Set.new([manager])
|
56
|
+
result.merge Entitlements.cache[:management_obj].direct_reports(manager)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Is someone in a management chain?
|
3
|
+
|
4
|
+
module Entitlements
|
5
|
+
module Extras
|
6
|
+
class Orgchart
|
7
|
+
class Rules
|
8
|
+
class Management < 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
|
+
begin
|
26
|
+
manager = Entitlements.cache[:people_obj].read(value)
|
27
|
+
rescue Entitlements::Data::People::NoSuchPersonError
|
28
|
+
# This is fatal. If this defines a team by a manager who is no longer in LDAP then the
|
29
|
+
# entry needs to be corrected because those people have to report to someone...
|
30
|
+
Entitlements.logger.fatal "Manager #{value} does not exist for file #{filename}!"
|
31
|
+
raise "Manager #{value} does not exist!"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Call all_reports which will return the set of all direct and indirect reports.
|
35
|
+
# This is evaluated once per run of the program.
|
36
|
+
Entitlements.cache[:management_obj] ||= begin
|
37
|
+
Entitlements::Extras::Orgchart::Logic.new(people: Entitlements.cache.fetch(:people_obj).read)
|
38
|
+
end
|
39
|
+
|
40
|
+
# This is fatal. If this defines a manager who has nobody reporting to them, then they
|
41
|
+
# aren't a manager at all. The entry should be changed to "username" or otherwise the
|
42
|
+
# proper manager should be filled in.
|
43
|
+
if Entitlements.cache[:management_obj].all_reports(manager).empty?
|
44
|
+
Entitlements.logger.fatal "Manager #{value} has no reports for file #{filename}!"
|
45
|
+
raise "Manager #{value} has no reports!"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Most of the time, people will expect "management: xyz" to include xyz and anyone who
|
49
|
+
# reports to them. Technically, xyz doesn't report to themself, but we'll hack it in here
|
50
|
+
# because it's least surprise. If someone really wants "xyz's reports but not xyz" they
|
51
|
+
# can use the "not" in conjunction.
|
52
|
+
result = Set.new([manager])
|
53
|
+
result.merge Entitlements.cache[:management_obj].all_reports(manager)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Loads extras either in this directory structure or elsewhere as specified by the end-user.
|
4
|
+
|
5
|
+
module Entitlements
|
6
|
+
module Extras
|
7
|
+
include ::Contracts::Core
|
8
|
+
C = ::Contracts
|
9
|
+
|
10
|
+
# Load and initialize extra functionality, whether in the `lib/extras` directory or in
|
11
|
+
# some other place provided by the user.
|
12
|
+
#
|
13
|
+
# namespace - String with the namespace in Entitlements::Extras to be loaded
|
14
|
+
# path - Optionally, a String with the directory where the Entitlements::Extras::<Namespace>::Base class can be found
|
15
|
+
#
|
16
|
+
# Returns the Class object for the base of the extra.
|
17
|
+
Contract String, C::Maybe[String] => Class
|
18
|
+
def self.load_extra(namespace, path = nil)
|
19
|
+
path ||= File.expand_path("./extras", __dir__)
|
20
|
+
unless File.file?(File.join(path, namespace, "base.rb"))
|
21
|
+
raise Errno::ENOENT, "Error loading #{namespace}: There is no file `base.rb` in directory `#{path}/#{namespace}`."
|
22
|
+
end
|
23
|
+
|
24
|
+
require File.join(path, namespace, "base.rb")
|
25
|
+
class_name = ["Entitlements", "Extras", Entitlements::Util::Util.camelize(namespace), "Base"].join("::")
|
26
|
+
clazz = Kernel.const_get(class_name)
|
27
|
+
clazz.init
|
28
|
+
|
29
|
+
# Register any rules defined by this class with the handler
|
30
|
+
register_rules(clazz)
|
31
|
+
|
32
|
+
# Register any additional methods on Entitlements::Models::Person
|
33
|
+
register_person_extra_methods(clazz)
|
34
|
+
|
35
|
+
# Record this extra's class as having been loaded.
|
36
|
+
Entitlements.record_loaded_extra(clazz)
|
37
|
+
|
38
|
+
# Contract return
|
39
|
+
@namespace_class ||= {}
|
40
|
+
@namespace_class[namespace] ||= clazz
|
41
|
+
end
|
42
|
+
|
43
|
+
# Register rules contained in this extra with a mapping of rules maintained by
|
44
|
+
# Entitlements::Data::Groups::Calculated::Base.
|
45
|
+
#
|
46
|
+
# clazz - Initialized Entitlements::Extras::<Namespace>::Base object
|
47
|
+
#
|
48
|
+
# Returns nothing.
|
49
|
+
Contract Class => nil
|
50
|
+
def self.register_rules(clazz)
|
51
|
+
return unless clazz.respond_to?(:rules)
|
52
|
+
|
53
|
+
clazz.rules.each do |rule_name|
|
54
|
+
rule_class_name = [clazz.to_s.sub(/::Base\z/, "::Rules"), Entitlements::Util::Util.camelize(rule_name)].join("::")
|
55
|
+
rule_class = Kernel.const_get(rule_class_name)
|
56
|
+
Entitlements::Data::Groups::Calculated.register_rule(rule_name, rule_class)
|
57
|
+
end
|
58
|
+
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
# Register methods for Entitlements::Models::Person that are contained in this extra
|
63
|
+
# with the Entitlements class.
|
64
|
+
#
|
65
|
+
# clazz - Initialized Entitlements::Extras::<Namespace>::Base object
|
66
|
+
#
|
67
|
+
# Returns nothing.
|
68
|
+
Contract Class => nil
|
69
|
+
def self.register_person_extra_methods(clazz)
|
70
|
+
return unless clazz.respond_to?(:person_methods)
|
71
|
+
|
72
|
+
clazz.person_methods.each do |method_name|
|
73
|
+
clazz_without_base = clazz.to_s.split("::")[0..-2]
|
74
|
+
method_class_name = [clazz_without_base, "PersonMethods"].join("::")
|
75
|
+
method_class = Kernel.const_get(method_class_name)
|
76
|
+
Entitlements.register_person_extra_method(method_name, method_class)
|
77
|
+
end
|
78
|
+
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Entitlements
|
4
|
+
class Models
|
5
|
+
class Action
|
6
|
+
include ::Contracts::Core
|
7
|
+
C = ::Contracts
|
8
|
+
|
9
|
+
# Constructor.
|
10
|
+
#
|
11
|
+
# dn - Distinguished name of action.
|
12
|
+
# existing - Current data according to data source.
|
13
|
+
# updated - Current data according to entitlements.
|
14
|
+
# ou - String with the OU as per entitlements.
|
15
|
+
# ignored_users - Optionally, a set of strings with users to ignore
|
16
|
+
Contract String, C::Or[nil, Entitlements::Models::Group, :none], C::Or[nil, Entitlements::Models::Group, Entitlements::Models::Person], String, C::KeywordArgs[ignored_users: C::Maybe[C::SetOf[String]]] => C::Any
|
17
|
+
def initialize(dn, existing, updated, ou, ignored_users: Set.new)
|
18
|
+
@dn = dn
|
19
|
+
@existing = existing
|
20
|
+
@updated = updated
|
21
|
+
@ou = ou
|
22
|
+
@implementation = nil
|
23
|
+
@ignored_users = ignored_users
|
24
|
+
end
|
25
|
+
|
26
|
+
# Element readers.
|
27
|
+
attr_reader :dn, :existing, :updated, :ou, :implementation, :ignored_users
|
28
|
+
|
29
|
+
# Determine if the change type is add, delete, or update.
|
30
|
+
Contract C::None => Symbol
|
31
|
+
def change_type
|
32
|
+
return :add if existing.nil?
|
33
|
+
return :delete if updated.nil?
|
34
|
+
:update
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get the configuration for the OU holding the group.
|
38
|
+
#
|
39
|
+
# Takes no arguments.
|
40
|
+
#
|
41
|
+
# Returns a configuration hash.
|
42
|
+
Contract C::None => C::HashOf[String => C::Any]
|
43
|
+
def config
|
44
|
+
Entitlements.config["groups"].fetch(ou)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Determine the type of the OU (defaults to ldap if undefined).
|
48
|
+
#
|
49
|
+
# Takes no arguments.
|
50
|
+
#
|
51
|
+
# Returns a string with the configuration type.
|
52
|
+
Contract C::None => String
|
53
|
+
def ou_type
|
54
|
+
config["type"] || "ldap"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Determine the short name of the DN.
|
58
|
+
#
|
59
|
+
# Takes no arguments.
|
60
|
+
#
|
61
|
+
# Returns a string with the short name of the DN.
|
62
|
+
Contract C::None => String
|
63
|
+
def short_name
|
64
|
+
dn =~ /\A(\w+)=(.+?),/ ? Regexp.last_match(2) : dn
|
65
|
+
end
|
66
|
+
|
67
|
+
# Add an implementation. This is for providers and services that do not have a 1:1 mapping
|
68
|
+
# between entitlements groups and implementing changes on the back end.
|
69
|
+
#
|
70
|
+
# data - Hash of Symbols with information that is meaningful to the back end.
|
71
|
+
#
|
72
|
+
# Returns nothing.
|
73
|
+
# :nocov:
|
74
|
+
Contract C::HashOf[Symbol => C::Any] => C::Any
|
75
|
+
def add_implementation(data)
|
76
|
+
@implementation ||= []
|
77
|
+
@implementation << data
|
78
|
+
end
|
79
|
+
# :nocov:
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,280 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Entitlements
|
4
|
+
class Models
|
5
|
+
class Group
|
6
|
+
include ::Contracts::Core
|
7
|
+
C = ::Contracts
|
8
|
+
|
9
|
+
class NoMembers < RuntimeError; end
|
10
|
+
class NoMetadata < RuntimeError; end
|
11
|
+
|
12
|
+
attr_reader :dn
|
13
|
+
|
14
|
+
# ------------------------------------------------------
|
15
|
+
# Constructor
|
16
|
+
# ------------------------------------------------------
|
17
|
+
|
18
|
+
# Constructor.
|
19
|
+
#
|
20
|
+
# dn - A String with the DN of the group
|
21
|
+
# members - A Set of Strings with the user IDs, or Entitlements::Models::Person, of the members
|
22
|
+
# description - Optionally, a String with a description
|
23
|
+
# metadata - Optionally, a Hash with [String => Object] metadata
|
24
|
+
Contract C::KeywordArgs[
|
25
|
+
dn: String,
|
26
|
+
members: C::SetOf[C::Or[Entitlements::Models::Person, String]],
|
27
|
+
description: C::Maybe[String],
|
28
|
+
metadata: C::Maybe[C::HashOf[String => C::Any]]
|
29
|
+
] => C::Any
|
30
|
+
def initialize(dn:, members:, description: nil, metadata: nil)
|
31
|
+
@dn = dn
|
32
|
+
set_members(members)
|
33
|
+
@description = description == [] ? "" : description
|
34
|
+
@metadata = metadata
|
35
|
+
end
|
36
|
+
|
37
|
+
# Constructor to copy another group object with a different DN.
|
38
|
+
#
|
39
|
+
# dn - The DN for the new group (must be != the DN for the source group)
|
40
|
+
#
|
41
|
+
# Returns Entitlements::Models::Group object.
|
42
|
+
Contract String => Entitlements::Models::Group
|
43
|
+
def copy_of(dn)
|
44
|
+
self.class.new(dn: dn, members: members.dup, description: description, metadata: metadata.dup)
|
45
|
+
end
|
46
|
+
|
47
|
+
# ------------------------------------------------------
|
48
|
+
# Tell us more about this group
|
49
|
+
# ------------------------------------------------------
|
50
|
+
|
51
|
+
# Description must be a string and cannot be empty. If description is empty just return
|
52
|
+
# the cn instead.
|
53
|
+
#
|
54
|
+
# Takes no arguments.
|
55
|
+
#
|
56
|
+
# Returns a non-empty string with the description.
|
57
|
+
Contract C::None => String
|
58
|
+
def description
|
59
|
+
return cn if @description.nil? || @description.empty?
|
60
|
+
@description
|
61
|
+
end
|
62
|
+
|
63
|
+
# Retrieve the members as a consistent object type (we'll pick Entitlements::Models::Person).
|
64
|
+
#
|
65
|
+
# people_obj - Entitlements::Data::People::* Object (required if not initialized with Entitlements::Models::Person's)
|
66
|
+
#
|
67
|
+
# Returns Set[Entitlements::Models::Person].
|
68
|
+
Contract C::KeywordArgs[
|
69
|
+
people_obj: C::Maybe[C::Any]
|
70
|
+
] => C::SetOf[Entitlements::Models::Person]
|
71
|
+
def members(people_obj: nil)
|
72
|
+
result = Set.new(
|
73
|
+
@members.map do |member|
|
74
|
+
if member.is_a?(Entitlements::Models::Person)
|
75
|
+
member
|
76
|
+
elsif people_obj.nil?
|
77
|
+
nil
|
78
|
+
elsif people_obj.read.key?(member)
|
79
|
+
people_obj.read(member)
|
80
|
+
else
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
end.compact
|
84
|
+
)
|
85
|
+
|
86
|
+
return result if result.any? || no_members_ok?
|
87
|
+
raise NoMembers, "The group #{dn} has no members!"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Retrieve the members as a string of DNs. This is a way to avoid converting to person objects if
|
91
|
+
# we don't need to do that anyway.
|
92
|
+
#
|
93
|
+
# Takes no arguments.
|
94
|
+
#
|
95
|
+
# Returns Set[String].
|
96
|
+
Contract C::None => C::SetOf[String]
|
97
|
+
def member_strings
|
98
|
+
@member_strings ||= begin
|
99
|
+
result = Set.new(@members.map { |member| member.is_a?(Entitlements::Models::Person) ? member.uid : member })
|
100
|
+
if result.empty? && !no_members_ok?
|
101
|
+
raise NoMembers, "The group #{dn} has no members!"
|
102
|
+
end
|
103
|
+
result
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Retrieve the members as a string of DNs, case-insensitive.
|
108
|
+
#
|
109
|
+
# Takes no arguments.
|
110
|
+
#
|
111
|
+
# Returns Set[String].
|
112
|
+
def member_strings_insensitive
|
113
|
+
@member_strings_insensitive ||= Set.new(member_strings.map(&:downcase))
|
114
|
+
end
|
115
|
+
|
116
|
+
# Determine if the given person is a member of the group.
|
117
|
+
#
|
118
|
+
# person - A Entitlements::Models::Person object
|
119
|
+
#
|
120
|
+
# Returns true if the person is a direct member of the group, false otherwise.
|
121
|
+
Contract C::Or[String, Entitlements::Models::Person] => C::Bool
|
122
|
+
def member?(person)
|
123
|
+
member_strings_insensitive.member?(any_to_uid(person).downcase)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Get the CN of the group (extracted from the DN).
|
127
|
+
#
|
128
|
+
# Takes no arguments.
|
129
|
+
#
|
130
|
+
# Returns a String with the CN.
|
131
|
+
Contract C::None => String
|
132
|
+
def cn
|
133
|
+
return Regexp.last_match(1) if dn =~ /\Acn=(.+?),/
|
134
|
+
raise "Could not determine CN from group DN #{dn.inspect}!"
|
135
|
+
end
|
136
|
+
|
137
|
+
# Retrieve the metadata, raising an error if no metadata was set.
|
138
|
+
#
|
139
|
+
# Takes no arguments.
|
140
|
+
#
|
141
|
+
# Returns a Hash with the metadata.
|
142
|
+
Contract C::None => C::HashOf[String => C::Any]
|
143
|
+
def metadata
|
144
|
+
return @metadata if @metadata
|
145
|
+
raise NoMetadata, "Group #{dn} was not constructed with metadata!"
|
146
|
+
end
|
147
|
+
|
148
|
+
# Determine if this group is equal to another Entitlements::Models::Group object.
|
149
|
+
#
|
150
|
+
# other_group - An Entitlements::Models::Group that is being evaluated against this one.
|
151
|
+
#
|
152
|
+
# Return true if the contents are equivalent, false otherwise.
|
153
|
+
Contract C::Or[Entitlements::Models::Person, Entitlements::Models::Group, :none] => C::Bool
|
154
|
+
def equals?(other_group)
|
155
|
+
unless other_group.is_a?(Entitlements::Models::Group)
|
156
|
+
return false
|
157
|
+
end
|
158
|
+
|
159
|
+
unless dn == other_group.dn
|
160
|
+
return false
|
161
|
+
end
|
162
|
+
|
163
|
+
unless description == other_group.description
|
164
|
+
return false
|
165
|
+
end
|
166
|
+
|
167
|
+
unless member_strings == other_group.member_strings
|
168
|
+
return false
|
169
|
+
end
|
170
|
+
|
171
|
+
true
|
172
|
+
end
|
173
|
+
|
174
|
+
alias_method :==, :equals?
|
175
|
+
|
176
|
+
# Retrieve a key from the metadata if the metadata is defined. Return nil if
|
177
|
+
# metadata wasn't defined. Don't raise an error.
|
178
|
+
#
|
179
|
+
# key - A String with the metadata key to retrieve.
|
180
|
+
#
|
181
|
+
# Returns the value of the metadata key or nil.
|
182
|
+
Contract String => C::Any
|
183
|
+
def metadata_fetch_if_exists(key)
|
184
|
+
return unless @metadata.is_a?(Hash)
|
185
|
+
@metadata[key]
|
186
|
+
end
|
187
|
+
|
188
|
+
# Determine if it's OK for the group to have no members. This is based on metadata, and defaults
|
189
|
+
# to true (it's OK to have no members). This can be overridden by setting metadata to false explicitly.
|
190
|
+
#
|
191
|
+
# Takes no arguments.
|
192
|
+
#
|
193
|
+
# Returns false if no_members_ok is explicitly set to false. Returns true otherwise.
|
194
|
+
Contract C::None => C::Bool
|
195
|
+
def no_members_ok?
|
196
|
+
![false, "false"].include?(metadata_fetch_if_exists("no_members_ok"))
|
197
|
+
end
|
198
|
+
|
199
|
+
# Directly manipulate members. This sets the group membership directly to the specified value with no
|
200
|
+
# verification or validation. Be very careful!
|
201
|
+
#
|
202
|
+
# members - A Set of Strings with the DNs, or Entitlements::Models::Person, of the members
|
203
|
+
#
|
204
|
+
# Returns nothing.
|
205
|
+
Contract C::SetOf[C::Or[Entitlements::Models::Person, String]] => nil
|
206
|
+
def set_members(members)
|
207
|
+
@members = members
|
208
|
+
@member_strings = nil
|
209
|
+
@member_strings_insensitive = nil
|
210
|
+
end
|
211
|
+
|
212
|
+
# Add a person to the member list of the group.
|
213
|
+
#
|
214
|
+
# person - Entitlements::Models::Person object.
|
215
|
+
#
|
216
|
+
# Returns nothing.
|
217
|
+
Contract C::Or[String, Entitlements::Models::Person] => nil
|
218
|
+
def add_member(person)
|
219
|
+
@members.add(person)
|
220
|
+
|
221
|
+
# Clear these so they will be recomputed
|
222
|
+
@member_strings = nil
|
223
|
+
@member_strings_insensitive = nil
|
224
|
+
end
|
225
|
+
|
226
|
+
# Remove a person from the member list of the group. This can act either on a person object
|
227
|
+
# or a distinguished name.
|
228
|
+
#
|
229
|
+
# person - Entitlements::Models::Person object or String with distinguished name.
|
230
|
+
#
|
231
|
+
# Returns nothing.
|
232
|
+
Contract C::Or[Entitlements::Models::Person, String] => nil
|
233
|
+
def remove_member(person)
|
234
|
+
person_uid = any_to_uid(person).downcase
|
235
|
+
@members.delete_if { |member| any_to_uid(member).downcase == person_uid }
|
236
|
+
|
237
|
+
# Clear these so they will be recomputed
|
238
|
+
@member_strings = nil
|
239
|
+
@member_strings_insensitive = nil
|
240
|
+
end
|
241
|
+
|
242
|
+
# Update the case of a member's distinguished name, if that person is a member of this group.
|
243
|
+
#
|
244
|
+
# person - Entitlements::Models::Person object or String with distinguished name (with the desired case).
|
245
|
+
#
|
246
|
+
# Returns true if a change was made, false if not. (Also returns false if the member isn't in the group.)
|
247
|
+
Contract C::Or[Entitlements::Models::Person, String] => C::Bool
|
248
|
+
def update_case(person)
|
249
|
+
person_uid = any_to_uid(person)
|
250
|
+
downcased_dn = person_uid.downcase
|
251
|
+
|
252
|
+
the_member = @members.find { |member| any_to_uid(member).downcase == downcased_dn }
|
253
|
+
return false unless the_member
|
254
|
+
return false if any_to_uid(the_member) == person_uid
|
255
|
+
|
256
|
+
remove_member(person_uid)
|
257
|
+
add_member(person_uid)
|
258
|
+
true
|
259
|
+
end
|
260
|
+
|
261
|
+
private
|
262
|
+
|
263
|
+
# Get a distinguished name from a String or an Entitlements::Models::Person object.
|
264
|
+
#
|
265
|
+
# obj - A String (with DN) or Entitlements::Models::Person
|
266
|
+
#
|
267
|
+
# Returns a String with the distinguished name.
|
268
|
+
Contract C::Any => String
|
269
|
+
def any_to_uid(obj)
|
270
|
+
if obj.is_a?(String)
|
271
|
+
return obj
|
272
|
+
elsif obj.is_a?(Entitlements::Models::Person)
|
273
|
+
return obj.uid
|
274
|
+
else
|
275
|
+
raise ArgumentError, "any_to_uid cannot handle #{obj.class}"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|