aker 3.0.0.pre

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 (118) hide show
  1. data/CHANGELOG.md +210 -0
  2. data/README.md +282 -0
  3. data/assets/aker/form/login.css +73 -0
  4. data/assets/aker/form/login.html.erb +44 -0
  5. data/lib/aker/authorities/automatic_access.rb +36 -0
  6. data/lib/aker/authorities/composite.rb +301 -0
  7. data/lib/aker/authorities/static.rb +283 -0
  8. data/lib/aker/authorities/support/find_sole_user.rb +24 -0
  9. data/lib/aker/authorities/support.rb +9 -0
  10. data/lib/aker/authorities.rb +46 -0
  11. data/lib/aker/cas/authority.rb +79 -0
  12. data/lib/aker/cas/configuration_helper.rb +85 -0
  13. data/lib/aker/cas/middleware/logout_responder.rb +49 -0
  14. data/lib/aker/cas/middleware/ticket_remover.rb +35 -0
  15. data/lib/aker/cas/middleware.rb +6 -0
  16. data/lib/aker/cas/proxy_mode.rb +108 -0
  17. data/lib/aker/cas/rack_proxy_callback.rb +188 -0
  18. data/lib/aker/cas/service_mode.rb +88 -0
  19. data/lib/aker/cas/service_url.rb +62 -0
  20. data/lib/aker/cas/user_ext.rb +64 -0
  21. data/lib/aker/cas.rb +31 -0
  22. data/lib/aker/central_parameters.rb +101 -0
  23. data/lib/aker/configuration.rb +534 -0
  24. data/lib/aker/deprecation.rb +105 -0
  25. data/lib/aker/form/custom_views_mode.rb +80 -0
  26. data/lib/aker/form/login_form_asset_provider.rb +56 -0
  27. data/lib/aker/form/middleware/custom_view_login_responder.rb +19 -0
  28. data/lib/aker/form/middleware/login_renderer.rb +72 -0
  29. data/lib/aker/form/middleware/login_responder.rb +71 -0
  30. data/lib/aker/form/middleware/logout_responder.rb +26 -0
  31. data/lib/aker/form/middleware.rb +10 -0
  32. data/lib/aker/form/mode.rb +118 -0
  33. data/lib/aker/form.rb +26 -0
  34. data/lib/aker/group.rb +67 -0
  35. data/lib/aker/group_membership.rb +162 -0
  36. data/lib/aker/ldap/authority.rb +392 -0
  37. data/lib/aker/ldap/user_ext.rb +19 -0
  38. data/lib/aker/ldap.rb +22 -0
  39. data/lib/aker/modes/base.rb +85 -0
  40. data/lib/aker/modes/http_basic.rb +100 -0
  41. data/lib/aker/modes/support/attempted_path.rb +22 -0
  42. data/lib/aker/modes/support/rfc_2617.rb +32 -0
  43. data/lib/aker/modes/support.rb +12 -0
  44. data/lib/aker/modes.rb +48 -0
  45. data/lib/aker/rack/authenticate.rb +37 -0
  46. data/lib/aker/rack/configuration_helper.rb +18 -0
  47. data/lib/aker/rack/default_logout_responder.rb +36 -0
  48. data/lib/aker/rack/environment_helper.rb +34 -0
  49. data/lib/aker/rack/facade.rb +102 -0
  50. data/lib/aker/rack/failure.rb +69 -0
  51. data/lib/aker/rack/logout.rb +63 -0
  52. data/lib/aker/rack/request_ext.rb +19 -0
  53. data/lib/aker/rack/session_timer.rb +95 -0
  54. data/lib/aker/rack/setup.rb +77 -0
  55. data/lib/aker/rack.rb +107 -0
  56. data/lib/aker/test/helpers.rb +22 -0
  57. data/lib/aker/test.rb +8 -0
  58. data/lib/aker/user.rb +231 -0
  59. data/lib/aker/version.rb +3 -0
  60. data/lib/aker.rb +51 -0
  61. data/spec/aker/aker-sample.yml +11 -0
  62. data/spec/aker/authorities/automatic_access_spec.rb +52 -0
  63. data/spec/aker/authorities/composite_spec.rb +488 -0
  64. data/spec/aker/authorities/nu-schema.jar +0 -0
  65. data/spec/aker/authorities/static_spec.rb +455 -0
  66. data/spec/aker/authorities/support/find_sole_user_spec.rb +33 -0
  67. data/spec/aker/authorities_spec.rb +16 -0
  68. data/spec/aker/cas/authority_spec.rb +106 -0
  69. data/spec/aker/cas/configuration_helper_spec.rb +92 -0
  70. data/spec/aker/cas/middleware/logout_responder_spec.rb +47 -0
  71. data/spec/aker/cas/middleware/ticket_remover_spec.rb +49 -0
  72. data/spec/aker/cas/proxy_mode_spec.rb +185 -0
  73. data/spec/aker/cas/rack_proxy_callback_spec.rb +190 -0
  74. data/spec/aker/cas/service_mode_spec.rb +122 -0
  75. data/spec/aker/cas/service_url_spec.rb +114 -0
  76. data/spec/aker/cas/user_ext_spec.rb +27 -0
  77. data/spec/aker/cas_spec.rb +19 -0
  78. data/spec/aker/central_parameters_spec.rb +44 -0
  79. data/spec/aker/configuration_spec.rb +465 -0
  80. data/spec/aker/deprecation_spec.rb +115 -0
  81. data/spec/aker/form/a_form_mode.rb +129 -0
  82. data/spec/aker/form/custom_views_mode_spec.rb +34 -0
  83. data/spec/aker/form/login_form_asset_provider_spec.rb +80 -0
  84. data/spec/aker/form/middleware/a_form_login_responder.rb +89 -0
  85. data/spec/aker/form/middleware/custom_view_login_responder_spec.rb +47 -0
  86. data/spec/aker/form/middleware/login_renderer_spec.rb +56 -0
  87. data/spec/aker/form/middleware/login_responder_spec.rb +34 -0
  88. data/spec/aker/form/middleware/logout_responder_spec.rb +55 -0
  89. data/spec/aker/form/mode_spec.rb +15 -0
  90. data/spec/aker/form_spec.rb +11 -0
  91. data/spec/aker/group_membership_spec.rb +208 -0
  92. data/spec/aker/group_spec.rb +66 -0
  93. data/spec/aker/ldap/authority_spec.rb +414 -0
  94. data/spec/aker/ldap/ldap-users.ldif +197 -0
  95. data/spec/aker/ldap_spec.rb +11 -0
  96. data/spec/aker/modes/a_aker_mode.rb +41 -0
  97. data/spec/aker/modes/http_basic_spec.rb +127 -0
  98. data/spec/aker/modes/support/attempted_path_spec.rb +32 -0
  99. data/spec/aker/modes_spec.rb +11 -0
  100. data/spec/aker/rack/authenticate_spec.rb +78 -0
  101. data/spec/aker/rack/default_logout_responder_spec.rb +67 -0
  102. data/spec/aker/rack/facade_spec.rb +154 -0
  103. data/spec/aker/rack/failure_spec.rb +151 -0
  104. data/spec/aker/rack/logout_spec.rb +63 -0
  105. data/spec/aker/rack/request_ext_spec.rb +29 -0
  106. data/spec/aker/rack/session_timer_spec.rb +134 -0
  107. data/spec/aker/rack/setup_spec.rb +87 -0
  108. data/spec/aker/rack_spec.rb +216 -0
  109. data/spec/aker/test/helpers_spec.rb +44 -0
  110. data/spec/aker/user_spec.rb +362 -0
  111. data/spec/aker_spec.rb +80 -0
  112. data/spec/deprecation_helper.rb +58 -0
  113. data/spec/java_helper.rb +5 -0
  114. data/spec/logger_helper.rb +17 -0
  115. data/spec/matchers.rb +31 -0
  116. data/spec/mock_builder.rb +25 -0
  117. data/spec/spec_helper.rb +52 -0
  118. metadata +265 -0
@@ -0,0 +1,56 @@
1
+ require 'aker/modes/support'
2
+ require 'erb'
3
+ require 'rack'
4
+
5
+ module Aker::Form
6
+ ##
7
+ # Provides HTML and CSS for login forms.
8
+ #
9
+ # @author David Yip
10
+ module LoginFormAssetProvider
11
+ include Rack::Utils
12
+ include Aker::Rack::ConfigurationHelper
13
+
14
+ ##
15
+ # Where to look for HTML and CSS assets.
16
+ #
17
+ # This is currently hardcoded as `(aker gem root)/assets/aker/form`.
18
+ #
19
+ # @return [String] a directory path
20
+ def asset_root
21
+ File.expand_path(File.join(File.dirname(__FILE__),
22
+ %w(.. .. ..),
23
+ %w(assets aker form)))
24
+ end
25
+
26
+ ##
27
+ # Provides the HTML for the login form.
28
+ #
29
+ # This method expects to find a `login.html.erb` ERB template in
30
+ # {#asset_root}. The ERB template is evaluated in an environment where
31
+ # a local variable named `script_name` is bound to the value of the
32
+ # `SCRIPT_NAME` Rack environment variable, which is useful for CSS and
33
+ # form action URL generation.
34
+ #
35
+ # @param env [Rack environment] a Rack environment
36
+ # @param [Hash] options rendering options
37
+ # @option options [Boolean] :login_failed If true, will render a failure message
38
+ # @option options [Boolean] :logged_out If true, will render a logout notification
39
+ # @option options [String] :username Text for the username field
40
+ # @option options [String] :url A URL to redirect to upon successful login
41
+ # @return [String] HTML data
42
+ def login_html(env, options = {})
43
+ login_base = env['SCRIPT_NAME'] + login_path(env)
44
+ template = File.read(File.join(asset_root, 'login.html.erb'))
45
+ ERB.new(template).result(binding)
46
+ end
47
+
48
+ ##
49
+ # Provides the CSS for the login form.
50
+ #
51
+ # @return [String] CSS data
52
+ def login_css
53
+ File.read(File.join(asset_root, 'login.css'))
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,19 @@
1
+ require 'aker'
2
+
3
+ module Aker::Form::Middleware
4
+ ##
5
+ # Extends {LoginResponder} to allow the application to re-render the
6
+ # login form when using {CustomViewsMode}.
7
+ class CustomViewLoginResponder < LoginResponder
8
+ protected
9
+
10
+ def unauthenticated(env)
11
+ request = ::Rack::Request.new(env)
12
+
13
+ env['aker.form.login_failed'] = true
14
+ env['aker.form.username'] = request['username']
15
+
16
+ @app.call(env)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,72 @@
1
+ require 'aker'
2
+
3
+ module Aker::Form::Middleware
4
+ ##
5
+ # Rack middleware used by {Aker::Form::Mode} to render an HTML login
6
+ # form.
7
+ #
8
+ # This middleware implements half of the form login process. The
9
+ # other half is implemented by {LoginResponder}.
10
+ #
11
+ # @author David Yip
12
+ class LoginRenderer
13
+ include Aker::Form::LoginFormAssetProvider
14
+ include Aker::Rack::ConfigurationHelper
15
+
16
+ ##
17
+ # Instantiates the middleware.
18
+ #
19
+ # @param app [Rack app] The Rack application on which this middleware
20
+ # should be layered.
21
+ # @param login_path [String] the login path
22
+ def initialize(app)
23
+ @app = app
24
+ end
25
+
26
+ ##
27
+ # Rack entry point.
28
+ #
29
+ # `call` returns one of three responses, depending on the path and
30
+ # method.
31
+ #
32
+ # * If the method is GET and the path is `login_path`, `call` returns
33
+ # an HTML form for submitting a username and password.
34
+ # * If the method is GET and the path is `login_path + "/login.css"`,
35
+ # `call` returns the CSS for the aforementioned form.
36
+ # * Otherwise, `call` passes the request down through the Rack stack.
37
+ #
38
+ # @return a finished Rack response
39
+ def call(env)
40
+ case [env['REQUEST_METHOD'], env['PATH_INFO']]
41
+ when ['GET', login_path(env)]; provide_login_html(env)
42
+ when ['GET', login_path(env) + '/login.css']; provide_login_css
43
+ else @app.call(env)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ ##
50
+ # An HTML form for logging in.
51
+ #
52
+ # @param env the Rack environment
53
+ # @return a finished Rack response
54
+ def provide_login_html(env)
55
+ request = ::Rack::Request.new(env)
56
+
57
+ ::Rack::Response.new(
58
+ login_html(env, :url => request['url'], :session_expired => request['session_expired'])
59
+ ).finish
60
+ end
61
+
62
+ ##
63
+ # CSS for the form provided by {provide_login_html}.
64
+ #
65
+ # @return a finished Rack response
66
+ def provide_login_css
67
+ ::Rack::Response.new(login_css) do |resp|
68
+ resp['Content-Type'] = 'text/css'
69
+ end.finish
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,71 @@
1
+ require 'aker'
2
+
3
+ module Aker::Form::Middleware
4
+ ##
5
+ # Rack middleware used by {Aker::Form::Mode} that finishes login
6
+ # requests by rendering a "Login successful" message.
7
+ #
8
+ # This middleware implements half of the form login process. The
9
+ # other half is implemented by {LoginRenderer}.
10
+ #
11
+ # @author David Yip
12
+ class LoginResponder
13
+ include Aker::Form::LoginFormAssetProvider
14
+ include Aker::Rack::ConfigurationHelper
15
+
16
+ ##
17
+ # Instantiates the middleware.
18
+ #
19
+ # @param app [Rack app] the Rack application on which this middleware
20
+ # should be layered
21
+ def initialize(app)
22
+ @app = app
23
+ end
24
+
25
+ ##
26
+ # Rack entry point. Responds to a `POST` to the configured login
27
+ # path.
28
+ #
29
+ # If the user is authenticated and a URL is given in the `url`
30
+ # parameter, then this action will redirect to `url`.
31
+ #
32
+ # @param env the Rack environment
33
+ # @return a finished Rack response
34
+ def call(env)
35
+ case [env['REQUEST_METHOD'], env['PATH_INFO']]
36
+ when ['POST', login_path(env)]; respond(env)
37
+ else @app.call(env)
38
+ end
39
+ end
40
+
41
+ protected
42
+
43
+ def respond(env)
44
+ warden = env['warden']
45
+
46
+ if !warden.authenticated?
47
+ warden.custom_failure!
48
+ unauthenticated(env)
49
+ else
50
+ redirect_to_target(env)
51
+ end
52
+ end
53
+
54
+ def unauthenticated(env)
55
+ request = Rack::Request.new(env)
56
+ body = login_html(env,
57
+ :login_failed => true,
58
+ :username => request['username'],
59
+ :url => request['url'])
60
+
61
+ ::Rack::Response.new(body, 401).finish
62
+ end
63
+
64
+ def redirect_to_target(env)
65
+ request = Rack::Request.new(env)
66
+ target = !(request['url'].blank?) ? request['url'] : request.env['SCRIPT_NAME'] + '/'
67
+
68
+ ::Rack::Response.new { |resp| resp.redirect(target) }.finish
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,26 @@
1
+ require 'aker'
2
+
3
+ module Aker::Form::Middleware
4
+ class LogoutResponder
5
+ include Aker::Form::LoginFormAssetProvider
6
+ include Aker::Rack::ConfigurationHelper
7
+
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ ##
13
+ # When given `GET` to the configured logout path, builds a Rack
14
+ # response containing the login form with a "you have been logged
15
+ # out" notification. Otherwise, passes the response on.
16
+ #
17
+ # @return a finished Rack response
18
+ def call(env)
19
+ if env['REQUEST_METHOD'] == 'GET' && env['PATH_INFO'] == logout_path(env)
20
+ ::Rack::Response.new(login_html(env, :logged_out => true)).finish
21
+ else
22
+ @app.call(env)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,10 @@
1
+ require 'aker'
2
+
3
+ module Aker::Form
4
+ module Middleware
5
+ autoload :CustomViewLoginResponder, 'aker/form/middleware/custom_view_login_responder'
6
+ autoload :LogoutResponder, 'aker/form/middleware/logout_responder'
7
+ autoload :LoginRenderer, 'aker/form/middleware/login_renderer'
8
+ autoload :LoginResponder, 'aker/form/middleware/login_responder'
9
+ end
10
+ end
@@ -0,0 +1,118 @@
1
+ require 'aker'
2
+ require 'uri'
3
+ require 'rack'
4
+
5
+ module Aker
6
+ module Form
7
+ ##
8
+ # An interactive mode that accepts a username and password POSTed from an
9
+ # HTML form.
10
+ #
11
+ # It expects the username in a `username` parameter and the unobfuscated
12
+ # password in a `password` parameter.
13
+ #
14
+ # By default, the form is rendered at and the credentials are
15
+ # received on '/login'; this can be overridden in the
16
+ # configuration like so:
17
+ #
18
+ # Aker.configure {
19
+ # rack_parameters :login_path => '/log-in-here'
20
+ # }
21
+ #
22
+ # This mode also renders said HTML form if authentication
23
+ # fails. Rendering is handled by by {Middleware::LoginRenderer}.
24
+ #
25
+ # @author David Yip
26
+ class Mode < Aker::Modes::Base
27
+ include ::Rack::Utils
28
+ include Aker::Modes::Support::AttemptedPath
29
+
30
+ ##
31
+ # A key that refers to this mode; used for configuration convenience.
32
+ #
33
+ # @return [Symbol]
34
+ def self.key
35
+ :form
36
+ end
37
+
38
+ ##
39
+ # Appends the {Middleware::LoginResponder login responder} to its
40
+ # position in the Rack middleware stack.
41
+ def self.append_middleware(builder)
42
+ builder.use(Middleware::LoginResponder)
43
+ builder.use(Middleware::LogoutResponder)
44
+ end
45
+
46
+ ##
47
+ # Prepends the {Middleware::LoginRenderer login form renderer} to
48
+ # its position in the Rack middleware stack.
49
+ def self.prepend_middleware(builder)
50
+ builder.use(Middleware::LoginRenderer)
51
+ end
52
+
53
+ ##
54
+ # The type of credentials supplied by this mode.
55
+ #
56
+ # @return [Symbol]
57
+ def kind
58
+ :user
59
+ end
60
+
61
+ ##
62
+ # Extracts username and password from request parameters.
63
+ #
64
+ # @return [Array<String>] username and password, username (if password
65
+ # missing), or an empty array
66
+ def credentials
67
+ [request['username'], request['password']].compact
68
+ end
69
+
70
+ ##
71
+ # Returns true if username and password are present, false otherwise.
72
+ def valid?
73
+ credentials.length == 2
74
+ end
75
+
76
+ ##
77
+ # The absolute URL for the login form.
78
+ #
79
+ # @return [String]
80
+ def login_url
81
+ uri = URI.parse(request.url)
82
+ uri.path = env['SCRIPT_NAME'] + login_path(configuration)
83
+ uri.to_s
84
+ end
85
+
86
+ ##
87
+ # Builds a Rack response that redirects to the login form.
88
+ #
89
+ # @return [Rack::Response]
90
+ def on_ui_failure
91
+ ::Rack::Response.new do |resp|
92
+ target = login_url + '?url=' + escape(attempted_path)
93
+ if env['aker.session_expired']
94
+ target += '&session_expired=true'
95
+ end
96
+ resp.redirect(target)
97
+ end
98
+ end
99
+
100
+ ##
101
+ # The path at which the login form will be accessible, as
102
+ # configured in the specified context.
103
+ #
104
+ # This path is specified relative to the application's mount point. If
105
+ # you're looking for the absolute URL of the login form, you need to use
106
+ # {#login_url}.
107
+ #
108
+ # @param [Aker::Configuration] configuration the configuration
109
+ # from which to derive the login path.
110
+ #
111
+ # @return [String]
112
+ def login_path(configuration)
113
+ configuration.parameters_for(:rack)[:login_path]
114
+ end
115
+ private :login_path
116
+ end
117
+ end
118
+ end
data/lib/aker/form.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'aker'
2
+
3
+ module Aker
4
+ ##
5
+ # The Aker mode that supports a traditional HTML login form, and its
6
+ # support infrastructure.
7
+ module Form
8
+ autoload :CustomViewsMode, 'aker/form/custom_views_mode'
9
+ autoload :LoginFormAssetProvider, 'aker/form/login_form_asset_provider'
10
+ autoload :Middleware, 'aker/form/middleware'
11
+ autoload :Mode, 'aker/form/mode'
12
+
13
+ ##
14
+ # @private
15
+ class Slice < Aker::Configuration::Slice
16
+ def initialize
17
+ super do
18
+ register_mode Mode
19
+ register_mode CustomViewsMode
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ Aker::Configuration.add_default_slice(Aker::Form::Slice.new)
data/lib/aker/group.rb ADDED
@@ -0,0 +1,67 @@
1
+ require 'tree'
2
+
3
+ require 'aker'
4
+
5
+ module Aker
6
+ ##
7
+ # The authority-independent representation of a group.
8
+ #
9
+ # Groups can be related in a tree. If so, a membership in an
10
+ # ancestor group implies membership in all its descendents.
11
+ #
12
+ # @see http://rubytree.rubyforge.org/rdoc/Tree/TreeNode.html
13
+ class Group < Tree::TreeNode
14
+ ##
15
+ # Creates a new group with the given name. You can add children
16
+ # using `<<`.
17
+ #
18
+ # @param [#to_s] name the desired name
19
+ # @param [Array,nil] args additional arguments. Included for
20
+ # marshalling compatibility with the base class.
21
+ def initialize(name, *args)
22
+ super # overridden to attach docs
23
+ end
24
+
25
+ ##
26
+ # Determines whether this group or any of its children matches the
27
+ # given parameter for authorization purposes.
28
+ #
29
+ # @param [#to_s,Group] other the thing to compare this
30
+ # group to
31
+ # @return [Boolean] true if the name of this group or any of its
32
+ # children is a case-insensitive match for the other.
33
+ def include?(other)
34
+ other_name =
35
+ case other
36
+ when Group; other.name;
37
+ else other.to_s;
38
+ end
39
+ self.find { |g| g.name.downcase == other_name.downcase }
40
+ end
41
+
42
+ ##
43
+ # Copy-pasted from parent in order to use appropriate class when
44
+ # deserializing children.
45
+ #
46
+ # @private
47
+ def marshal_load(dumped_tree_array)
48
+ nodes = { }
49
+
50
+ for node_hash in dumped_tree_array do
51
+ name = node_hash[:name]
52
+ parent_name = node_hash[:parent]
53
+ content = Marshal.load(node_hash[:content])
54
+
55
+ if parent_name then
56
+ nodes[name] = current_node = self.class.new(name, content)
57
+ nodes[parent_name].add current_node
58
+ else
59
+ # This is the root node, hence initialize self.
60
+ initialize(name, content)
61
+
62
+ nodes[name] = self # Add self to the list of nodes
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,162 @@
1
+ require 'aker'
2
+
3
+ module Aker
4
+ ##
5
+ # The authority-independent representation of a user's association
6
+ # with a particular group, possibly constrained by affiliate.
7
+ class GroupMembership
8
+ ##
9
+ # The affiliate IDs to which this membership is scoped. If this
10
+ # array is blank or nil, the membership applies to all affiliates.
11
+ #
12
+ # An "affiliate" is an arbitrary scope designator for a
13
+ # membership. The specific form will depend on the authority that
14
+ # is authorizing the user.
15
+ #
16
+ # @return [Array<Object>]
17
+ attr_accessor :affiliate_ids
18
+
19
+ ##
20
+ # Create a new instance.
21
+ #
22
+ # @param [Group] group the group for which this object records
23
+ # membership
24
+ def initialize(group)
25
+ @group = group
26
+ end
27
+
28
+ ##
29
+ # Determines whether this membership applies to the given
30
+ # affiliate.
31
+ #
32
+ # @param [Object] affiliate_id
33
+ # @return [Boolean]
34
+ def include_affiliate?(affiliate_id)
35
+ affiliate_ids.blank? ? true : affiliate_ids.include?(affiliate_id)
36
+ end
37
+
38
+ ##
39
+ # @return [String] the name of the group for which this object
40
+ # indicates membership.
41
+ def group_name
42
+ self.group.name
43
+ end
44
+
45
+ ##
46
+ # @return [Group] the group for which this is a membership
47
+ def group
48
+ @group
49
+ end
50
+
51
+ def affiliate_ids
52
+ @affiliate_ids ||= []
53
+ end
54
+ end
55
+
56
+ ##
57
+ # An authority-independent collection of all the group memberships
58
+ # for a particular user at a particular portal.
59
+ class GroupMemberships < Array
60
+ ##
61
+ # The portal for which all these group memberships apply.
62
+ #
63
+ # @return [Symbol]
64
+ attr_reader :portal
65
+
66
+ # n.b.: if you add more attributes, be sure to add them to the
67
+ # custom serialization.
68
+
69
+ ##
70
+ # Create a new instance.
71
+ #
72
+ # @param [#to_sym] portal
73
+ def initialize(portal)
74
+ @portal = portal.to_sym
75
+ end
76
+
77
+ ##
78
+ # Determines whether this collection indicates that the user is
79
+ # authorized in the the given group, possibly constrained by one
80
+ # or more affiliates.
81
+ #
82
+ # (Note that this method hides the superclass `include?` method.)
83
+ #
84
+ # @param [Group,#to_s] group the group in question or its name
85
+ # @param [Array<Object>,nil] *affiliate_ids the affiliates to use to
86
+ # constrain the query.
87
+ #
88
+ # @return [Boolean] true so long as the user is authorized in
89
+ # `group` for **at least one** of the specified affiliates. If
90
+ # no affiliates are specified, only the groups themselves are
91
+ # considered.
92
+ def include?(group, *affiliate_ids)
93
+ !find(group, *affiliate_ids).empty?
94
+ end
95
+
96
+ ##
97
+ # Finds the group memberships that match the given group, possibly
98
+ # constrained by one or more affiliates.
99
+ #
100
+ # (Note that this method hides the `Enumerable` method `find`.
101
+ # You can still use it under its `detect` alias.)
102
+ #
103
+ # @param [Group,#to_s] group the group in question or its name
104
+ # @param [Array<Object>,nil] *affiliate_ids the affiliates to use to
105
+ # constrain the query.
106
+ #
107
+ # @return [Array<GroupMembership>]
108
+ def find(group, *affiliate_ids)
109
+ candidates = self.select { |gm| gm.group.include?(group) }
110
+ return candidates if affiliate_ids.empty?
111
+ candidates.select { |gm| affiliate_ids.detect { |id| gm.include_affiliate?(id) } }
112
+ end
113
+
114
+ ##
115
+ # Custom serialization for this array. Needed because we need to
116
+ # serialize the full tree for all referenced groups in order to be
117
+ # able to do {#include?} and {#find} correctly on the deserialized
118
+ # result.
119
+ #
120
+ # @return [Hash] suitable for passing to {#marshal_load}
121
+ def marshal_dump
122
+ {
123
+ :group_roots => find_group_roots,
124
+ :memberships => dump_gm_hashes,
125
+ :portal => portal
126
+ }
127
+ end
128
+
129
+ ##
130
+ # Custom deserialization for this array. Reverses
131
+ # {#marshal_dump}.
132
+ #
133
+ # @return [void]
134
+ def marshal_load(dump)
135
+ @portal = dump[:portal]
136
+ roots = dump[:group_roots]
137
+ dump[:memberships].each do |gm_hash|
138
+ self << GroupMembership.new(find_group_from_roots(gm_hash[:group_name], roots)).
139
+ tap { |gm| gm.affiliate_ids.concat(gm_hash[:affiliate_ids]) }
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def find_group_from_roots(group_name, roots)
146
+ roots.each do |root|
147
+ root.each do |group|
148
+ return group if group.name == group_name
149
+ end
150
+ end
151
+ raise "Could not find #{group_name} in any of the roots (#{roots.inspect})"
152
+ end
153
+
154
+ def find_group_roots
155
+ self.collect { |gm| gm.group.root }.uniq
156
+ end
157
+
158
+ def dump_gm_hashes
159
+ self.collect { |gm| { :group_name => gm.group_name, :affiliate_ids => gm.affiliate_ids } }
160
+ end
161
+ end
162
+ end