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