nitro-auth 0.2.0
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.
- data/CHANGELOG +12 -0
- data/LICENSE +31 -0
- data/README +115 -0
- data/TODO +7 -0
- data/examples/basic/public/error.xhtml +81 -0
- data/examples/basic/public/js/behaviour.js +254 -0
- data/examples/basic/public/js/controls.js +446 -0
- data/examples/basic/public/js/dragdrop.js +537 -0
- data/examples/basic/public/js/effects.js +612 -0
- data/examples/basic/public/js/prototype.js +1038 -0
- data/examples/basic/public/register.xhtml +65 -0
- data/examples/basic/run.rb +32 -0
- data/examples/basic/src/basic.rb +4 -0
- data/examples/basic/src/basic/auth_controller.rb +9 -0
- data/examples/basic/src/basic/controller.rb +29 -0
- data/examples/basic/src/basic/site.rb +13 -0
- data/examples/basic/src/basic/user.rb +22 -0
- data/examples/basic/src/basic/view/index.xhtml +35 -0
- data/lib/nitro/auth.rb +41 -0
- data/lib/nitro/auth/auth_controller.rb +199 -0
- data/lib/nitro/auth/controller.rb +166 -0
- data/lib/nitro/auth/model/user.rb +147 -0
- data/lib/nitro/auth/util/crypt.rb +55 -0
- data/lib/nitro/auth/view/access_denied.xhtml +13 -0
- data/lib/nitro/auth/view/login.xhtml +38 -0
- data/lib/nitro/auth/view/logout.xhtml +16 -0
- data/lib/nitro/auth/view/register.xhtml +51 -0
- metadata +90 -0
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
require 'glue/configuration'
|
4
|
+
|
5
|
+
module Auth
|
6
|
+
# Include this class in any controller that you want to have
|
7
|
+
# authentication and/or authorization on.
|
8
|
+
module Controller
|
9
|
+
# The Auth::User object for the currently logged-in user.
|
10
|
+
# Will be +nil+ if no user is logged in.
|
11
|
+
def user
|
12
|
+
# If we don't have a user yet, see if we can get one via
|
13
|
+
# the session key cookie.
|
14
|
+
if not @user
|
15
|
+
session_key = request.cookies['login_session_key']
|
16
|
+
if session_key
|
17
|
+
@user = User.find_one(:where => "session_key = '#{session_key}'")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# If we already had a user, or managed to find one above,
|
22
|
+
# check for session expiration.
|
23
|
+
if @user
|
24
|
+
if @user.session_key_expired?
|
25
|
+
@expired_login = @user.login
|
26
|
+
@user = nil
|
27
|
+
else
|
28
|
+
@expired_login = nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
@user
|
33
|
+
end
|
34
|
+
@user = nil
|
35
|
+
|
36
|
+
# Is the current user an administrator?
|
37
|
+
def administrator?
|
38
|
+
user and user.has_role? Auth.admin_role
|
39
|
+
end
|
40
|
+
|
41
|
+
# Is the current user allowed to execute the current action?
|
42
|
+
def allowed?
|
43
|
+
required = required_roles[action_name.intern]
|
44
|
+
not required or (user and user.has_role? required)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Checks the current user's permission to run the current action,
|
48
|
+
# and redirects to the appropriate auth action if a login is needed
|
49
|
+
# or if the current user doesn't have sufficient permissions.
|
50
|
+
def check_permissions
|
51
|
+
if not allowed?
|
52
|
+
store_location
|
53
|
+
redirect "/auth/access_denied" if user
|
54
|
+
redirect URI.escape("/auth/login?login=#{@expired_login}")
|
55
|
+
raise RenderExit
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Stores the current location, so that we can redirect the user
|
60
|
+
# to a login page but get back to where they originally wanted to go.
|
61
|
+
def store_location
|
62
|
+
session["prelogin_uri"] = request.uri
|
63
|
+
session["prelogin_referer"] = request.referer
|
64
|
+
end
|
65
|
+
|
66
|
+
# Spits out a link to the login page if there is no current user,
|
67
|
+
# or to the logout page if there is one.
|
68
|
+
def login_link
|
69
|
+
unless user
|
70
|
+
body.a "Login", :href => "/auth/login"
|
71
|
+
else
|
72
|
+
body.a "Logout", :href => "/auth/logout"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
# If the user had an expired session key, this was their login
|
78
|
+
# name. Allows being a little friendlier when redirecting and
|
79
|
+
# forcing a new login.
|
80
|
+
@expired_login = nil
|
81
|
+
|
82
|
+
def self.append_features(base)
|
83
|
+
base.module_eval {
|
84
|
+
private
|
85
|
+
# Make sure that @@required_roles is a class variable
|
86
|
+
# on the controller class, and not on Auth::Controller.
|
87
|
+
@@required_roles = Hash.new
|
88
|
+
def self.required_roles
|
89
|
+
@@required_roles
|
90
|
+
end
|
91
|
+
def required_roles
|
92
|
+
@@required_roles
|
93
|
+
end
|
94
|
+
|
95
|
+
# @@protected_methods also needs to be a class variable
|
96
|
+
# on the controller class.
|
97
|
+
@@protected_methods = Array.new
|
98
|
+
def self.protected_methods
|
99
|
+
@@protected_methods
|
100
|
+
end
|
101
|
+
|
102
|
+
# Make sure that protected methods don't get included
|
103
|
+
# in the list of action methods.
|
104
|
+
def self.action_methods
|
105
|
+
am = super
|
106
|
+
am.reject { |method|
|
107
|
+
@@protected_methods.include? method
|
108
|
+
}
|
109
|
+
end
|
110
|
+
}
|
111
|
+
|
112
|
+
super
|
113
|
+
base.extend(ClassMethods)
|
114
|
+
end
|
115
|
+
|
116
|
+
module ClassMethods
|
117
|
+
# Protects the given action.
|
118
|
+
# Any requests to call it will require login
|
119
|
+
# and will check permissions automatically.
|
120
|
+
# The original implementation will be "hidden"
|
121
|
+
# from Nitro so that it cannot be called directly.
|
122
|
+
#
|
123
|
+
# [+action+] The action to protect.
|
124
|
+
# [+:role+] The required role name.
|
125
|
+
# (Defaults to Auth.user_role.)
|
126
|
+
#
|
127
|
+
# The default role is essentially equivalent to
|
128
|
+
# "must be authenticated", as all users have the
|
129
|
+
# user role by default.
|
130
|
+
def protect(action, options = {})
|
131
|
+
role = options[:role] || Auth.user_role
|
132
|
+
required_role action, role, options
|
133
|
+
end
|
134
|
+
|
135
|
+
# Protects the given action and requires
|
136
|
+
# administrative privileges to call it.
|
137
|
+
def administrative(action, options = {})
|
138
|
+
required_role action, Auth.admin_role, options
|
139
|
+
end
|
140
|
+
|
141
|
+
# Sets the role required to run the given action,
|
142
|
+
# and makes the action protected, so that any requests
|
143
|
+
# to call it will require login and check permissions
|
144
|
+
# automatically.
|
145
|
+
#
|
146
|
+
# [+action+] The action to protect.
|
147
|
+
# [+role+] The required role name.
|
148
|
+
def required_role(action, role, options = {})
|
149
|
+
action = action.intern if action.is_a? String
|
150
|
+
role = role.to_s
|
151
|
+
required_roles[action] = role
|
152
|
+
|
153
|
+
unprot_action = "unprotected_" + action.to_s
|
154
|
+
protected_methods << unprot_action
|
155
|
+
|
156
|
+
alias_method unprot_action, action
|
157
|
+
class_eval %{
|
158
|
+
def #{action}
|
159
|
+
check_permissions
|
160
|
+
#{unprot_action}
|
161
|
+
end
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'og'
|
2
|
+
require 'glue/logger'
|
3
|
+
|
4
|
+
module Auth
|
5
|
+
# Represents a user or account.
|
6
|
+
# Has a many-to-many relationship with Auth::Role.
|
7
|
+
#
|
8
|
+
# This class can be either extended or related to in order to
|
9
|
+
# create application users. This allows a 'user' to own other
|
10
|
+
# application objects, for example, without polluting the basic
|
11
|
+
# authentication/authorization code.
|
12
|
+
#
|
13
|
+
# Your application's user object's #create method should:
|
14
|
+
# * Have a signature like create(login, password, parameters = {})
|
15
|
+
# Note that parameters is essentially request.parameters from the
|
16
|
+
# registration form, and thus should be treated as tainted.
|
17
|
+
# * Call super(login, password, parameters) if it extends Auth::User
|
18
|
+
# * Call Auth::User.create(login, password, parameters) if it relates
|
19
|
+
# to Auth::User rather than extending it.
|
20
|
+
class User
|
21
|
+
#--
|
22
|
+
# These are done as attrs and then as props, so that we
|
23
|
+
# can get them to show up in rdoc. Ugly but it seems to work.
|
24
|
+
#++
|
25
|
+
|
26
|
+
# The user's login name.
|
27
|
+
attr :login
|
28
|
+
# The user's salted and hashed password.
|
29
|
+
attr :hashed_password
|
30
|
+
# The last salt used, for later password checks.
|
31
|
+
attr :salt
|
32
|
+
# The security session key, which can be stored in a browser cookie
|
33
|
+
# to allow later password-less login.
|
34
|
+
#
|
35
|
+
# Getting the session key will create a new session key if the
|
36
|
+
# old one has expired (and thus cookie checks will fail,
|
37
|
+
# as they should.)
|
38
|
+
attr :session_key
|
39
|
+
# Time when the session key expires.
|
40
|
+
attr :session_key_expires
|
41
|
+
|
42
|
+
prop_reader :login, String, :unique => true,
|
43
|
+
:sql => 'varchar(255) not null unique'
|
44
|
+
prop_reader :hashed_password, String,
|
45
|
+
:sql => 'char(40) null'
|
46
|
+
prop :session_key, String, :sql_index => true,
|
47
|
+
:sql => 'char(40) null',
|
48
|
+
:reader => false, :writer => false
|
49
|
+
prop_reader :session_key_expires, Time
|
50
|
+
prop :salt, String, :sql => 'char(40) null',
|
51
|
+
:reader => false, :writer => false
|
52
|
+
|
53
|
+
many_to_many Role
|
54
|
+
|
55
|
+
schema_inheritance
|
56
|
+
|
57
|
+
# Set the raw password. Will appropriately hash it and store it
|
58
|
+
# in +hashed_password+.
|
59
|
+
def password=(new_password)
|
60
|
+
if not new_password
|
61
|
+
@salt = nil
|
62
|
+
@hashed_password = nil
|
63
|
+
else
|
64
|
+
@salt = Crypt.make_salt
|
65
|
+
@hashed_password = Crypt.salt_password @salt, new_password
|
66
|
+
end
|
67
|
+
update if @oid
|
68
|
+
end
|
69
|
+
|
70
|
+
def session_key # :nodoc:
|
71
|
+
if session_key_expired?
|
72
|
+
@session_key = Crypt.make_session_key hashed_password
|
73
|
+
@session_key_expires = Time.now + Auth.session_key_expiration
|
74
|
+
update if @oid
|
75
|
+
end
|
76
|
+
@session_key
|
77
|
+
end
|
78
|
+
# Has the session key expired?
|
79
|
+
def session_key_expired?
|
80
|
+
not @session_key or
|
81
|
+
(session_key_expires and Time.now > session_key_expires)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Convenience method. Does this user have this role?
|
85
|
+
#
|
86
|
+
# Can take either a Auth::Role object or a symbol/string role name.
|
87
|
+
def has_role?(role)
|
88
|
+
if role.is_a? Role
|
89
|
+
roles.include? role
|
90
|
+
else
|
91
|
+
# This is the canonical implementation
|
92
|
+
# roles.include? Role.find_one(:where => "name = '#{role.to_s}'")
|
93
|
+
# This is sort of a hack, but turns two queries into one.
|
94
|
+
not find_roles(:extra => "AND #{Role.table}.name = '#{role.to_s}'").empty?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Creates a new user. Login is required, password is optional.
|
99
|
+
# Currently, parameters is not used by this implementation.
|
100
|
+
# Auth::AuthController passes in request.params, though,
|
101
|
+
# so future implementations could get further information there.
|
102
|
+
# Subclasses can use it and should pass it along.
|
103
|
+
def initialize(login, password = nil, parameters = {})
|
104
|
+
@login = login
|
105
|
+
self.password = password
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Represents a (simple) security role.
|
110
|
+
#
|
111
|
+
# Many-to-many with Auth::User. An Auth::User can have many
|
112
|
+
# +Roles+ and an Auth::Role can be had by many +Users+.
|
113
|
+
class Role
|
114
|
+
#--
|
115
|
+
# These are done as attrs and then as props, so that we
|
116
|
+
# can get them to show up in rdoc. Ugly but it seems to work.
|
117
|
+
#++
|
118
|
+
|
119
|
+
# The role name. (Must be unique.)
|
120
|
+
attr :name
|
121
|
+
|
122
|
+
property :name, String, :sql => 'varchar(255) not null unique',
|
123
|
+
:unique => true
|
124
|
+
many_to_many User
|
125
|
+
|
126
|
+
schema_inheritance
|
127
|
+
|
128
|
+
post "create_roles", :on => :og_create_schema
|
129
|
+
|
130
|
+
# Note that if you subclass Auth::Role, you need to allow just
|
131
|
+
# passing in a role name and nothing else in your initialize
|
132
|
+
# (and your schema), or you need to override the create_roles
|
133
|
+
# method.
|
134
|
+
def initialize(name)
|
135
|
+
@name = name
|
136
|
+
end
|
137
|
+
|
138
|
+
# Create the admin and user roles.
|
139
|
+
def create_roles
|
140
|
+
# I believe that because this is called in an advice,
|
141
|
+
# that self.class will be the right class for the entity
|
142
|
+
# that actually got created, even if it's not us.
|
143
|
+
self.class.create(Auth.admin_role) if 0 == self.class.count(:condition => "name = '#{Auth.admin_role}'")
|
144
|
+
self.class.create(Auth.user_role) if 0 == self.class.count(:condition => "name = '#{Auth.user_role}'")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
module Auth
|
4
|
+
# Cryptographic utilities used by the Auth module.
|
5
|
+
module Crypt
|
6
|
+
#--
|
7
|
+
# Ideally, I'd want the next bit to be settings, but they're not
|
8
|
+
# behaving quite like I wanted. Ah, well.
|
9
|
+
# FIXME: Figure out some way to get them into configs or something.
|
10
|
+
#
|
11
|
+
# setting :crypt_prefix, :default => '-CHANGE-ME-',
|
12
|
+
# :doc => 'Prefix used when hashing any string.'
|
13
|
+
# setting :salt_prefix, :default => '-salt-',
|
14
|
+
# :doc => 'Prefix used when generating the salt.'
|
15
|
+
#++
|
16
|
+
protected
|
17
|
+
@@crypt_prefix = "-CHANGE-ME-"
|
18
|
+
@@salt_prefix = "-salt-"
|
19
|
+
@@session_key_expiration = 60*60*24*30
|
20
|
+
def self.crypt_prefix
|
21
|
+
@@crypt_prefix
|
22
|
+
end
|
23
|
+
def self.salt_prefix
|
24
|
+
@@salt_prefix
|
25
|
+
end
|
26
|
+
|
27
|
+
public
|
28
|
+
# Creates a timestamp string for inclusion in hashed strings.
|
29
|
+
def self.timestamp
|
30
|
+
Time.now.strftime '%Y%m%d-%H%M%S'
|
31
|
+
end
|
32
|
+
|
33
|
+
# Hashes the provided string. Returns a 40-character
|
34
|
+
# hex representation of the hashed value.
|
35
|
+
def self.make_hash(value)
|
36
|
+
Digest::SHA1.hexdigest "#{crypt_prefix}--#{value}--"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Create a salt for mixing with hashed strings.
|
40
|
+
def self.make_salt
|
41
|
+
make_hash "#{salt_prefix}-#{timestamp}"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Mix a salt into a password and return the hashed result.
|
45
|
+
def self.salt_password(salt, password)
|
46
|
+
make_hash salt + password
|
47
|
+
end
|
48
|
+
|
49
|
+
# Create a session key, given a password.
|
50
|
+
# Use the salted/hashed password here, not the raw one!
|
51
|
+
def self.make_session_key(hashed_password)
|
52
|
+
make_hash hashed_password + timestamp + rand.to_s
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE html
|
3
|
+
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
4
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
5
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
6
|
+
<head>
|
7
|
+
<title>Access Denied</title>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
<h1>Access Denied</h1>
|
11
|
+
<p><a href="#{@backlink}">Go back</a>.</p>
|
12
|
+
</body>
|
13
|
+
</html>
|
@@ -0,0 +1,38 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE html
|
3
|
+
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
4
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
5
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
6
|
+
<head>
|
7
|
+
<title>Please log in.</title>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
<h1>Please log in.</h1>
|
11
|
+
<form action = "#{try_login_url}" method = "POST">
|
12
|
+
<table>
|
13
|
+
<tr>
|
14
|
+
<td><label for="login">Login:</label></td>
|
15
|
+
<td>
|
16
|
+
<input id="login" name="login" type="text"
|
17
|
+
value="#{@login_name}"/>
|
18
|
+
</td>
|
19
|
+
</tr>
|
20
|
+
<tr>
|
21
|
+
<td><label for="password">Password:</label></td>
|
22
|
+
<td><input id="password" name="password" type="password"/></td>
|
23
|
+
</tr>
|
24
|
+
<tr>
|
25
|
+
<td colspan="2">
|
26
|
+
<button type="submit">Login!</button>
|
27
|
+
</td>
|
28
|
+
</tr>
|
29
|
+
<tr>
|
30
|
+
<td id="error" colspan="2">#{@error}</td>
|
31
|
+
</tr>
|
32
|
+
</table>
|
33
|
+
</form>
|
34
|
+
<p class="register_link">
|
35
|
+
<a href="#{register_url}">Register a new account</a>.
|
36
|
+
</p>
|
37
|
+
</body>
|
38
|
+
</html>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE html
|
3
|
+
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
4
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
5
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
6
|
+
<head>
|
7
|
+
<title>Logged out.</title>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
<h1>Logged out.</h1>
|
11
|
+
<p>Thanks!</p>
|
12
|
+
<p class="login_link">
|
13
|
+
<a href="#{login_url}">Log back in</a>.
|
14
|
+
</p>
|
15
|
+
</body>
|
16
|
+
</html>
|
@@ -0,0 +1,51 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE html
|
3
|
+
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
4
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
5
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
6
|
+
<head>
|
7
|
+
<title>Register a new account.</title>
|
8
|
+
<meta http-equiv="Content-Script-Type" content="text/javascript"/>
|
9
|
+
#{include_script "/js/behaviour.js"}
|
10
|
+
</head>
|
11
|
+
<body>
|
12
|
+
<h1>Register a new account.</h1>
|
13
|
+
<form id="new_user_form" action="#{new_user_url}" method="POST">
|
14
|
+
<table>
|
15
|
+
<tr>
|
16
|
+
<td><label for="login">Login:</label></td>
|
17
|
+
<td>
|
18
|
+
<input id="login" name="login" type="text"
|
19
|
+
value="#{@new_user.login}"/>
|
20
|
+
</td>
|
21
|
+
</tr>
|
22
|
+
<tr>
|
23
|
+
<td><label for="password">Password:</label></td>
|
24
|
+
<td><input id="password" name="password" type="password"/></td>
|
25
|
+
</tr>
|
26
|
+
<tr>
|
27
|
+
<td><label for="confirm_password">Confirm Password:</label></td>
|
28
|
+
<td>
|
29
|
+
<input id="confirm_password" name="confirm_password"
|
30
|
+
type="password"/>
|
31
|
+
</td>
|
32
|
+
</tr>
|
33
|
+
<tr>
|
34
|
+
<td colspan="2">
|
35
|
+
<button id="register_new_user" type="button">Register!</button>
|
36
|
+
</td>
|
37
|
+
</tr>
|
38
|
+
<tr>
|
39
|
+
<td id="error" colspan="2">#{@error}</td>
|
40
|
+
</tr>
|
41
|
+
</table>
|
42
|
+
</form>
|
43
|
+
<p class="login_link">
|
44
|
+
<a href="#{login_url}">Go back to the login page</a>.
|
45
|
+
</p>
|
46
|
+
<p class="back_link">
|
47
|
+
<a href="#{@backlink}">Go back to where I started</a>.
|
48
|
+
</p>
|
49
|
+
#{helper_script}
|
50
|
+
</body>
|
51
|
+
</html>
|