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,65 @@
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="name">Name:</label></td>
24
+ <td>
25
+ <input id="name" name="name" type="text"
26
+ value="#{@new_user.name}"/>
27
+ </td>
28
+ </tr>
29
+ <tr>
30
+ <td><label for="email">Email Address:</label></td>
31
+ <td>
32
+ <input id="email" name="email" type="text"
33
+ value="#{@new_user.email}"/>
34
+ </td>
35
+ </tr>
36
+ <tr>
37
+ <td><label for="password">Password:</label></td>
38
+ <td><input id="password" name="password" type="password"/></td>
39
+ </tr>
40
+ <tr>
41
+ <td><label for="confirm_password">Confirm Password:</label></td>
42
+ <td>
43
+ <input id="confirm_password" name="confirm_password"
44
+ type="password"/>
45
+ </td>
46
+ </tr>
47
+ <tr>
48
+ <td colspan="2">
49
+ <button id="register_new_user" type="button">Register!</button>
50
+ </td>
51
+ </tr>
52
+ <tr>
53
+ <td id="error" colspan="2">#{@error}</td>
54
+ </tr>
55
+ </table>
56
+ </form>
57
+ <p class="login_link">
58
+ <a href="#{login_url}">Go back to the login page</a>.
59
+ </p>
60
+ <p class="back_link">
61
+ <a href="#{@backlink}">Go back to where I started</a>.
62
+ </p>
63
+ #{helper_script}
64
+ </body>
65
+ </html>
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+ require 'nitro'
3
+
4
+ require 'basic'
5
+
6
+ include Nitro
7
+
8
+ # Because it's not synced by default on Windows
9
+ STDERR.sync = true if not STDERR.sync
10
+
11
+ # Assuming you set up a database with these values...
12
+ db_config = {
13
+ :store => 'mysql',
14
+ :name => 'basic',
15
+ :user => 'basic',
16
+ :password => 'basic'
17
+ }
18
+ Og.setup(db_config)
19
+
20
+ # Tell the AuthController which User class to create
21
+ # on new-user registration.
22
+ Auth::AuthController.user_class = Basic::User
23
+
24
+ # Set up the dispatcher, including an AuthController
25
+ # at /auth.
26
+ Server.map = {
27
+ '/' => Basic::BasicController,
28
+ '/auth' => Basic::AuthController
29
+ }
30
+
31
+ # Run the example.
32
+ Nitro.run()
@@ -0,0 +1,4 @@
1
+ require 'basic/user'
2
+ require 'basic/site'
3
+ require 'basic/controller'
4
+ require 'basic/auth_controller'
@@ -0,0 +1,9 @@
1
+ require 'nitro/auth'
2
+
3
+ module Basic
4
+ # We want to provide some custom templates, so we need to tell
5
+ # Nitro where to look for them.
6
+ class AuthController < Auth::AuthController
7
+ @template_root = "public"
8
+ end
9
+ end
@@ -0,0 +1,29 @@
1
+ require 'nitro'
2
+ require 'nitro/auth'
3
+
4
+ module Basic
5
+ class BasicController < Nitro::Controller
6
+ include Auth::Controller
7
+
8
+ @template_root = File.dirname(__FILE__) + "/view"
9
+
10
+ scaffold Site, :index => true
11
+
12
+ def list_site
13
+ if user.has_role? Auth.admin_role
14
+ @sites = Site.all()
15
+ else
16
+ @sites = Site.find(:where => "owner_oid = #{user.oid}")
17
+ end
18
+ end
19
+ protect :index
20
+ protect :list_site
21
+ protect :edit_site
22
+ protect :view_site
23
+ protect :save_site
24
+ protect :del_site
25
+ protect :delete_site
26
+
27
+ administrative :new_site
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ require 'og'
2
+
3
+ module Basic
4
+ # A sample application object, just to illustrate.
5
+ class Site
6
+ property :url, String, :sql => "varchar(255) not null unique",
7
+ :unique => true
8
+ property :title, String, :sql => "varchar(255) not null unique",
9
+ :unique => true
10
+
11
+ belongs_to :owner, User
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ require 'nitro/auth'
2
+
3
+ module Basic
4
+ # An extended user object.
5
+ class User < Auth::User
6
+ # Add name and email fields.
7
+ property :name, String, :sql => "varchar(255) not null"
8
+ property :email, String, :sql => "varchar(255) not null unique",
9
+ :unique => true
10
+
11
+ # Add a relationship with an application object.
12
+ has_many Site
13
+
14
+ def initialize(login = nil, password = nil, parameters = {})
15
+ super(login, password, parameters)
16
+ # Should probably sanity check these, since they're coming
17
+ # in over the web, but this'll do for now.
18
+ @name = parameters["name"]
19
+ @email = parameters["email"]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
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>Basic Nitro-Auth Example</title>
8
+ </head>
9
+ <body>
10
+ <h1>Basic Nitro-Auth Example</h1>
11
+ <table>
12
+ <tr>
13
+ <td class="sidebar">
14
+ <a href="/">List sites</a>
15
+ <?r if user.has_role? Auth.admin_role ?>
16
+ <br/><a href="/new_site">Add a new site</a>
17
+ <?r end ?>
18
+ </td>
19
+ <td>
20
+ <table>
21
+ <?r for site in @sites ?>
22
+ <tr>
23
+ <td><a href="#{site.url}">#{site.title}</a></td>
24
+ <td>Located at #{site.url}</td>
25
+ <td if="user.has_role? Auth.admin_role">
26
+ Owned by #{site.owner.name}
27
+ </td>
28
+ </tr>
29
+ <?r end ?>
30
+ </table>
31
+ </td>
32
+ </tr>
33
+ </table>
34
+ </body>
35
+ </html>
@@ -0,0 +1,41 @@
1
+ require 'glue'
2
+
3
+ # Auth provides authentication and authorization for Nitro.
4
+ module Auth
5
+ Version = '0.2.0'
6
+
7
+ #--
8
+ # Things I wish were settings, but setting doesn't appear to do
9
+ # what I'm needing...
10
+ # FIXME: Figure out some sane way to put these in config.
11
+ #
12
+ # setting :admin_role, :default => 'admin',
13
+ # :doc => 'The administrative/superuser role name.'
14
+ # setting :user_role, :default => 'user',
15
+ # :doc => 'The default role name (all users get this by default).'
16
+ # setting :session_key_expiration, :default => 60 * 60 * 24 * 30,
17
+ # :doc => 'Number of seconds session keys last before expiring.'
18
+ #++
19
+
20
+ # The administrative/superuser role name.
21
+ def self.admin_role
22
+ 'admin'
23
+ end
24
+
25
+ # The default role name (all users get this by default).
26
+ def self.user_role
27
+ 'user'
28
+ end
29
+
30
+ # Number of seconds session keys last before expiring.
31
+ def self.session_key_expiration
32
+ 60*60*24*30
33
+ end
34
+ end
35
+
36
+ require 'nitro/auth/util/crypt'
37
+
38
+ require 'nitro/auth/model/user'
39
+
40
+ require 'nitro/auth/controller'
41
+ require 'nitro/auth/auth_controller'
@@ -0,0 +1,199 @@
1
+ require 'uri'
2
+
3
+ require 'glue/uri'
4
+
5
+ require 'nitro'
6
+ require 'nitro/mixin/javascript'
7
+
8
+ module Auth
9
+ # Provides basic login actions.
10
+ #
11
+ # In theory, +AuthController+ can be easily integrated into your
12
+ # application. Or, at least, that's the goal -- we're early enough
13
+ # that it hasn't been tested much yet.
14
+ class AuthController < Nitro::Controller
15
+ include Nitro::JavascriptMixin
16
+
17
+ # Override the template_root for this controller.
18
+ @template_root = File.dirname(__FILE__) + "/view"
19
+
20
+ # Spits out the url to the register-a-new-user page.
21
+ def register_url
22
+ "#{controller_name}/register"
23
+ end
24
+
25
+ # Spits out the url to the login page.
26
+ def login_url
27
+ "#{controller_name}/login"
28
+ end
29
+
30
+ # Spits out the url to the try-to-login action.
31
+ def try_login_url
32
+ "#{controller_name}/try_login"
33
+ end
34
+
35
+ # Spits out the url to the create-a-new-user action.
36
+ def new_user_url
37
+ "#{controller_name}/new_user"
38
+ end
39
+
40
+ # The action run when the current user doesn't have sufficient
41
+ # permissions to run the requested action.
42
+ def access_denied
43
+ @backlink = session["prelogin_referer"]
44
+ session.delete "prelogin_uri"
45
+ session.delete "prelogin_referer"
46
+ end
47
+
48
+ # The main login action.
49
+ def login
50
+ @login_name = request.params["login"].to_s
51
+ @login_name = nil if @login_name.empty?
52
+ @error = "Login or password incorrect." if "false" == request.params["allowed"]
53
+ @error = request.params["error"] if request.params["error"]
54
+ end
55
+
56
+ # Log the current user out.
57
+ def logout
58
+ Logger.debug "Logging out."
59
+ session_cookie = Cookie.new "login_session_key", ""
60
+ response.add_cookie session_cookie
61
+ end
62
+
63
+ # Attempt to log the user in. This is the action at which
64
+ # login forms should point (preferably using try_login_url).
65
+ #
66
+ # Redirects back wherever they originally came from if login
67
+ # is successful, or the login page again if not.
68
+ def try_login
69
+ Logger.debug("Trying to log in.")
70
+ user = nil
71
+ login = request.params['login']
72
+ password = request.params['password']
73
+ user = User.find_one(:where => "login = '#{login}'") if login
74
+ Logger.debug("Found user #{user.oid} with login #{login}.") if user
75
+
76
+ if user and authenticate(user, password)
77
+ session_key = user.session_key
78
+ if session_key
79
+ session_cookie = Cookie.new "login_session_key", session_key
80
+ session_cookie.expires = user.session_key_expires
81
+ response.add_cookie session_cookie
82
+ end
83
+ Logger.debug("Sending them back from whence they came.")
84
+ return_to_original_location
85
+ end
86
+
87
+ Logger.debug("Login incorrect, sending them back to login page.")
88
+ redirect login_url + "?allowed=false"
89
+ end
90
+
91
+ # Returns the user back to their original location.
92
+ def return_to_original_location
93
+ prelogin_uri = session["prelogin_uri"]
94
+ session.delete "prelogin_uri"
95
+ session.delete "prelogin_referer"
96
+ Logger.debug("Redirecting back to #{prelogin_uri}.")
97
+ redirect prelogin_uri if prelogin_uri
98
+
99
+ Logger.debug("No prelogin URI, redirecting back to / instead.")
100
+ redirect "/"
101
+ end
102
+
103
+
104
+ # Register a new user.
105
+ #
106
+ # Expects a few things of its template:
107
+ # * Form action should point to new_user (using new_user_url).
108
+ # * Needs, at least:
109
+ # * Text input named login.
110
+ # * Password input named password.
111
+ # * Password input named confirm_password.
112
+ # * error (a text element for displaying errors,
113
+ # should generally be rendered with #{@error} or the like).
114
+ # * Form submit button named register_new_user.
115
+ # * Template should use the Nitro javascript mixin appropriately.
116
+ def register
117
+ @backlink = session["prelogin_referer"]
118
+ @error = request.params["error"] || ""
119
+ @new_user = @@user_class.new(nil, nil, request.params)
120
+ behaviour "#register_new_user", %{
121
+ el.onclick = function() {
122
+ var form = this.form;
123
+ var error = document.getElementById("error");
124
+ if (0 == form.login.value.length) {
125
+ error.innerHTML = "You must provide a login."
126
+ } else if (0 == form.password.value.length) {
127
+ error.innerHTML = "You must provide a password."
128
+ } else if (form.password.value != form.confirm_password.value) {
129
+ error.innerHTML = "Passwords do not match."
130
+ } else {
131
+ form.submit();
132
+ }
133
+ }
134
+ }
135
+ end
136
+
137
+ # Try to create a new user. This is the action at which
138
+ # "register a new user" forms should point.
139
+ #
140
+ # TODO: Allow some sort of password quality check? A plugin, perhaps?
141
+ # Or just assume that the user object's validation is sufficient?
142
+ #
143
+ # TODO: Use Nitro form validation once it's all there.
144
+ #
145
+ # TODO: Get rid of these hardcoded error messages.
146
+ def new_user
147
+ Logger.debug("Setting up new user.")
148
+ login = request.params['login']
149
+ password = request.params['password']
150
+ confirm = request.params['confirm_password']
151
+ # Doublecheck. The Javascript should have checked this, but
152
+ # just in case...
153
+ registration_error "You must provide a login." if login.empty?
154
+ registration_error "You must provide a password." if password.empty?
155
+ registration_error "Passwords do not match." if password != confirm
156
+ # Further validation is delegated to the user object.
157
+
158
+ # See if the login is duplicate
159
+ existing_user = User.find_one(:where => "login = '#{login}'")
160
+ registration_error "Login #{login} already exists." if existing_user
161
+
162
+ # Okay, try to create it.
163
+ new_user = @@user_class.create(login, password, request.params)
164
+ user_role = Role.find_one(:where => "name = '#{Auth.user_role}'")
165
+ new_user.add_role user_role if user_role
166
+ Logger.debug("Finished new user setup, trying to log in.")
167
+ try_login
168
+ end
169
+
170
+ #--
171
+ # FIXME: This is sort of a hack for now. Ugh.
172
+ #++
173
+
174
+ # Sets the default class all authentication controllers will use
175
+ # when creating new users.
176
+ #
177
+ # Uses Auth::User by default, but generally applications will
178
+ # extend that class rather than use it bare.
179
+ def self.user_class=(klass)
180
+ if klass.is_a? Class
181
+ @@user_class = klass
182
+ else
183
+ raise "Invalid default user_class set in Auth::AuthController."
184
+ end
185
+ end
186
+ @@user_class = Auth::User
187
+
188
+ protected
189
+ # Subclasses can override this to use some other authentication
190
+ # mechanism (LDAP or what have you).
191
+ def authenticate(user, password) # :doc:
192
+ user.hashed_password == Crypt.salt_password(user.salt, password)
193
+ end
194
+ private
195
+ def registration_error(error)
196
+ redirect URI.escape("#{register_url}?error=#{error}")
197
+ end
198
+ end
199
+ end