entitlements 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
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