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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/VERSION +1 -0
  3. data/bin/deploy-entitlements +18 -0
  4. data/lib/entitlements/auditor/base.rb +163 -0
  5. data/lib/entitlements/backend/base_controller.rb +171 -0
  6. data/lib/entitlements/backend/base_provider.rb +55 -0
  7. data/lib/entitlements/backend/dummy/controller.rb +89 -0
  8. data/lib/entitlements/backend/dummy.rb +3 -0
  9. data/lib/entitlements/backend/ldap/controller.rb +188 -0
  10. data/lib/entitlements/backend/ldap/provider.rb +128 -0
  11. data/lib/entitlements/backend/ldap.rb +4 -0
  12. data/lib/entitlements/backend/member_of/controller.rb +203 -0
  13. data/lib/entitlements/backend/member_of.rb +3 -0
  14. data/lib/entitlements/cli.rb +121 -0
  15. data/lib/entitlements/data/groups/cached.rb +120 -0
  16. data/lib/entitlements/data/groups/calculated/base.rb +478 -0
  17. data/lib/entitlements/data/groups/calculated/filters/base.rb +93 -0
  18. data/lib/entitlements/data/groups/calculated/filters/member_of_group.rb +32 -0
  19. data/lib/entitlements/data/groups/calculated/modifiers/base.rb +38 -0
  20. data/lib/entitlements/data/groups/calculated/modifiers/expiration.rb +56 -0
  21. data/lib/entitlements/data/groups/calculated/ruby.rb +137 -0
  22. data/lib/entitlements/data/groups/calculated/rules/base.rb +35 -0
  23. data/lib/entitlements/data/groups/calculated/rules/group.rb +129 -0
  24. data/lib/entitlements/data/groups/calculated/rules/username.rb +41 -0
  25. data/lib/entitlements/data/groups/calculated/text.rb +337 -0
  26. data/lib/entitlements/data/groups/calculated/yaml.rb +171 -0
  27. data/lib/entitlements/data/groups/calculated.rb +290 -0
  28. data/lib/entitlements/data/groups.rb +13 -0
  29. data/lib/entitlements/data/people/combined.rb +197 -0
  30. data/lib/entitlements/data/people/dummy.rb +71 -0
  31. data/lib/entitlements/data/people/ldap.rb +142 -0
  32. data/lib/entitlements/data/people/yaml.rb +102 -0
  33. data/lib/entitlements/data/people.rb +58 -0
  34. data/lib/entitlements/extras/base.rb +40 -0
  35. data/lib/entitlements/extras/ldap_group/base.rb +20 -0
  36. data/lib/entitlements/extras/ldap_group/filters/member_of_ldap_group.rb +50 -0
  37. data/lib/entitlements/extras/ldap_group/rules/ldap_group.rb +69 -0
  38. data/lib/entitlements/extras/orgchart/base.rb +32 -0
  39. data/lib/entitlements/extras/orgchart/logic.rb +171 -0
  40. data/lib/entitlements/extras/orgchart/person_methods.rb +55 -0
  41. data/lib/entitlements/extras/orgchart/rules/direct_report.rb +62 -0
  42. data/lib/entitlements/extras/orgchart/rules/management.rb +59 -0
  43. data/lib/entitlements/extras.rb +82 -0
  44. data/lib/entitlements/models/action.rb +82 -0
  45. data/lib/entitlements/models/group.rb +280 -0
  46. data/lib/entitlements/models/person.rb +149 -0
  47. data/lib/entitlements/plugins/dummy.rb +22 -0
  48. data/lib/entitlements/plugins/group_of_names.rb +28 -0
  49. data/lib/entitlements/plugins/posix_group.rb +46 -0
  50. data/lib/entitlements/plugins.rb +13 -0
  51. data/lib/entitlements/rule/base.rb +74 -0
  52. data/lib/entitlements/service/ldap.rb +405 -0
  53. data/lib/entitlements/util/mirror.rb +42 -0
  54. data/lib/entitlements/util/override.rb +64 -0
  55. data/lib/entitlements/util/util.rb +219 -0
  56. data/lib/entitlements.rb +606 -0
  57. 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