aker 3.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +210 -0
- data/README.md +282 -0
- data/assets/aker/form/login.css +73 -0
- data/assets/aker/form/login.html.erb +44 -0
- data/lib/aker/authorities/automatic_access.rb +36 -0
- data/lib/aker/authorities/composite.rb +301 -0
- data/lib/aker/authorities/static.rb +283 -0
- data/lib/aker/authorities/support/find_sole_user.rb +24 -0
- data/lib/aker/authorities/support.rb +9 -0
- data/lib/aker/authorities.rb +46 -0
- data/lib/aker/cas/authority.rb +79 -0
- data/lib/aker/cas/configuration_helper.rb +85 -0
- data/lib/aker/cas/middleware/logout_responder.rb +49 -0
- data/lib/aker/cas/middleware/ticket_remover.rb +35 -0
- data/lib/aker/cas/middleware.rb +6 -0
- data/lib/aker/cas/proxy_mode.rb +108 -0
- data/lib/aker/cas/rack_proxy_callback.rb +188 -0
- data/lib/aker/cas/service_mode.rb +88 -0
- data/lib/aker/cas/service_url.rb +62 -0
- data/lib/aker/cas/user_ext.rb +64 -0
- data/lib/aker/cas.rb +31 -0
- data/lib/aker/central_parameters.rb +101 -0
- data/lib/aker/configuration.rb +534 -0
- data/lib/aker/deprecation.rb +105 -0
- data/lib/aker/form/custom_views_mode.rb +80 -0
- data/lib/aker/form/login_form_asset_provider.rb +56 -0
- data/lib/aker/form/middleware/custom_view_login_responder.rb +19 -0
- data/lib/aker/form/middleware/login_renderer.rb +72 -0
- data/lib/aker/form/middleware/login_responder.rb +71 -0
- data/lib/aker/form/middleware/logout_responder.rb +26 -0
- data/lib/aker/form/middleware.rb +10 -0
- data/lib/aker/form/mode.rb +118 -0
- data/lib/aker/form.rb +26 -0
- data/lib/aker/group.rb +67 -0
- data/lib/aker/group_membership.rb +162 -0
- data/lib/aker/ldap/authority.rb +392 -0
- data/lib/aker/ldap/user_ext.rb +19 -0
- data/lib/aker/ldap.rb +22 -0
- data/lib/aker/modes/base.rb +85 -0
- data/lib/aker/modes/http_basic.rb +100 -0
- data/lib/aker/modes/support/attempted_path.rb +22 -0
- data/lib/aker/modes/support/rfc_2617.rb +32 -0
- data/lib/aker/modes/support.rb +12 -0
- data/lib/aker/modes.rb +48 -0
- data/lib/aker/rack/authenticate.rb +37 -0
- data/lib/aker/rack/configuration_helper.rb +18 -0
- data/lib/aker/rack/default_logout_responder.rb +36 -0
- data/lib/aker/rack/environment_helper.rb +34 -0
- data/lib/aker/rack/facade.rb +102 -0
- data/lib/aker/rack/failure.rb +69 -0
- data/lib/aker/rack/logout.rb +63 -0
- data/lib/aker/rack/request_ext.rb +19 -0
- data/lib/aker/rack/session_timer.rb +95 -0
- data/lib/aker/rack/setup.rb +77 -0
- data/lib/aker/rack.rb +107 -0
- data/lib/aker/test/helpers.rb +22 -0
- data/lib/aker/test.rb +8 -0
- data/lib/aker/user.rb +231 -0
- data/lib/aker/version.rb +3 -0
- data/lib/aker.rb +51 -0
- data/spec/aker/aker-sample.yml +11 -0
- data/spec/aker/authorities/automatic_access_spec.rb +52 -0
- data/spec/aker/authorities/composite_spec.rb +488 -0
- data/spec/aker/authorities/nu-schema.jar +0 -0
- data/spec/aker/authorities/static_spec.rb +455 -0
- data/spec/aker/authorities/support/find_sole_user_spec.rb +33 -0
- data/spec/aker/authorities_spec.rb +16 -0
- data/spec/aker/cas/authority_spec.rb +106 -0
- data/spec/aker/cas/configuration_helper_spec.rb +92 -0
- data/spec/aker/cas/middleware/logout_responder_spec.rb +47 -0
- data/spec/aker/cas/middleware/ticket_remover_spec.rb +49 -0
- data/spec/aker/cas/proxy_mode_spec.rb +185 -0
- data/spec/aker/cas/rack_proxy_callback_spec.rb +190 -0
- data/spec/aker/cas/service_mode_spec.rb +122 -0
- data/spec/aker/cas/service_url_spec.rb +114 -0
- data/spec/aker/cas/user_ext_spec.rb +27 -0
- data/spec/aker/cas_spec.rb +19 -0
- data/spec/aker/central_parameters_spec.rb +44 -0
- data/spec/aker/configuration_spec.rb +465 -0
- data/spec/aker/deprecation_spec.rb +115 -0
- data/spec/aker/form/a_form_mode.rb +129 -0
- data/spec/aker/form/custom_views_mode_spec.rb +34 -0
- data/spec/aker/form/login_form_asset_provider_spec.rb +80 -0
- data/spec/aker/form/middleware/a_form_login_responder.rb +89 -0
- data/spec/aker/form/middleware/custom_view_login_responder_spec.rb +47 -0
- data/spec/aker/form/middleware/login_renderer_spec.rb +56 -0
- data/spec/aker/form/middleware/login_responder_spec.rb +34 -0
- data/spec/aker/form/middleware/logout_responder_spec.rb +55 -0
- data/spec/aker/form/mode_spec.rb +15 -0
- data/spec/aker/form_spec.rb +11 -0
- data/spec/aker/group_membership_spec.rb +208 -0
- data/spec/aker/group_spec.rb +66 -0
- data/spec/aker/ldap/authority_spec.rb +414 -0
- data/spec/aker/ldap/ldap-users.ldif +197 -0
- data/spec/aker/ldap_spec.rb +11 -0
- data/spec/aker/modes/a_aker_mode.rb +41 -0
- data/spec/aker/modes/http_basic_spec.rb +127 -0
- data/spec/aker/modes/support/attempted_path_spec.rb +32 -0
- data/spec/aker/modes_spec.rb +11 -0
- data/spec/aker/rack/authenticate_spec.rb +78 -0
- data/spec/aker/rack/default_logout_responder_spec.rb +67 -0
- data/spec/aker/rack/facade_spec.rb +154 -0
- data/spec/aker/rack/failure_spec.rb +151 -0
- data/spec/aker/rack/logout_spec.rb +63 -0
- data/spec/aker/rack/request_ext_spec.rb +29 -0
- data/spec/aker/rack/session_timer_spec.rb +134 -0
- data/spec/aker/rack/setup_spec.rb +87 -0
- data/spec/aker/rack_spec.rb +216 -0
- data/spec/aker/test/helpers_spec.rb +44 -0
- data/spec/aker/user_spec.rb +362 -0
- data/spec/aker_spec.rb +80 -0
- data/spec/deprecation_helper.rb +58 -0
- data/spec/java_helper.rb +5 -0
- data/spec/logger_helper.rb +17 -0
- data/spec/matchers.rb +31 -0
- data/spec/mock_builder.rb +25 -0
- data/spec/spec_helper.rb +52 -0
- 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
|