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.
@@ -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>