Merge lp:~malept/loggerhead/standalone-auth into lp:loggerhead

Proposed by Mark Lee
Status: Work in progress
Proposed branch: lp:~malept/loggerhead/standalone-auth
Merge into: lp:loggerhead
Diff against target: 240 lines (+189/-0)
4 files modified
NEWS (+3/-0)
loggerhead/config.py (+11/-0)
loggerhead/htpasswd.py (+139/-0)
loggerhead/main.py (+36/-0)
To merge this branch: bzr merge lp:~malept/loggerhead/standalone-auth
Reviewer Review Type Date Requested Status
Krisztian Eyssen (community) Approve
Max Kanat-Alexander (community) Needs Fixing
Michael Hudson-Doyle Approve
Review via email: mp+18828@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Mark Lee (malept) wrote :

Please see bug 518724 for description/rationale.

Revision history for this message
Matt Nordhoff (mnordhoff) wrote :

You should add the bug number to NEWS (but someone can do that when landing it).

Also, the single-letter variable names are icky, though coming up with 3 different username variables is a pain.

Other than that, this looks good to me, but I'm not smart enough to know if it really is. It's great that you were able to leverage Paste and Trac to avoid writing any unnecessary code. :)

Revision history for this message
Mark Lee (malept) wrote :

> You should add the bug number to NEWS (but someone can do that when landing
> it).

Fixed in revision 405. I wasn't entirely sure whether to add the bug report number, as the dev entries in NEWS are a bit inconsistent in that respect.

> Also, the single-letter variable names are icky, though coming up with 3
> different username variables is a pain.

Yeah, it is :) I attempted to fix this in revision 406.

> Other than that, this looks good to me, but I'm not smart enough to know if it
> really is. It's great that you were able to leverage Paste and Trac to avoid
> writing any unnecessary code. :)

Thanks for the review.

405. By Mark Lee

NEWS: add bug number.

406. By Mark Lee

Replace single-character variable names; add comments.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

It looks good to me, and very simple testing indicates it works. It's a pleasingly small amount of code!

The only thing I'd have you change is to move the import statements to the top of the file.

Thanks for your contribution!

Oh, can I get you to do the contributor agreement thing? http://www.canonical.com/contributors

review: Approve
407. By Mark Lee

Move import statements to the top of the file.

Revision history for this message
Mark Lee (malept) wrote :

> The only thing I'd have you change is to move the import statements to the top
> of the file.

OK. I wanted to avoid importing code when it's not being used, but this is OK too, as it's not really an extra dependency. Fixed in revision 407.

> Oh, can I get you to do the contributor agreement thing?
> http://www.canonical.com/contributors

I'll read + sign it later tonight.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Did you ever get to the contributor agreement?

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Ping?

Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

I find it hard to believe that the code in the htpasswd module here doesn't already exist in some external python library. Isn't there already some standard method for storing passwords, using a python library, that we can use without having to write our own?

Also, the code that is currently in main.py should probably be mostly within a middleware class.

review: Needs Fixing
Revision history for this message
John A Meinel (jameinel) wrote :

AFAIK, we're still waiting on a contributor agreement, and Max has some concerns about the implementation. I'm thinking to kick this back to Work-in-Progress, but I'll try to wait a day or two to see if it just got forgotten.

Revision history for this message
Krisztian Eyssen (eyssen) :
review: Approve
Revision history for this message
Graham Binns (gmb) wrote :

Since there doesn't seem to have been any change to this I'm going to move it back to Work In Progress as John suggests above.

Revision history for this message
Richie Ward (richies) wrote :

I have rebased this on master (rev 451):
https://code.launchpad.net/~richies/+junk/standalone_auth

Unmerged revisions

407. By Mark Lee

Move import statements to the top of the file.

406. By Mark Lee

Replace single-character variable names; add comments.

405. By Mark Lee

NEWS: add bug number.

404. By Mark Lee

Typo/PEP8 fixes.

403. By Mark Lee

Split out the authentication realm option into its own command-line flag.

402. By Mark Lee

Add support for HTTP digest authentication in standalone mode.

401. By Mark Lee

Add support for HTTP basic authentication in standalone mode.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2010-01-11 14:17:45 +0000
+++ NEWS 2010-02-08 20:46:16 +0000
@@ -12,6 +12,9 @@
1212
13 - Support FastCGI, SCGI and AJP using flup. (Denis Martinez)13 - Support FastCGI, SCGI and AJP using flup. (Denis Martinez)
1414
15 - Add support for HTTP basic/digest authentication in standalone
16 mode. (Mark Lee, #518724)
17
151.17 [20Aug2009]181.17 [20Aug2009]
16---------------19---------------
1720
1821
=== modified file 'loggerhead/config.py'
--- loggerhead/config.py 2009-11-23 22:45:46 +0000
+++ loggerhead/config.py 2010-02-08 20:46:16 +0000
@@ -49,6 +49,17 @@
49 parser.add_option("--protocol", dest="protocol",49 parser.add_option("--protocol", dest="protocol",
50 help=("Protocol to use: http, scgi, fcgi, ajp"50 help=("Protocol to use: http, scgi, fcgi, ajp"
51 "(defaults to http)."))51 "(defaults to http)."))
52 parser.add_option("--auth-realm", dest="auth_realm", metavar="REALM",
53 help="The authentication realm used by Loggerhead "
54 "when HTTP authentication is enabled.")
55 parser.add_option("--digest-auth", dest="digest_auth", metavar="PATH",
56 help="Enables HTTP Digest authentication support, "
57 "PATH being the absolute path to the htdigest "
58 "file (requires --auth-realm).")
59 parser.add_option("--basic-auth", dest="basic_auth", metavar="PATH",
60 help="Enables HTTP Basic authentication support, "
61 "PATH being the absolute path to the htpasswd "
62 "file (requires --auth-realm).")
52 parser.add_option("--memory-profile", action="store_true",63 parser.add_option("--memory-profile", action="store_true",
53 help="Profile the memory usage using Dozer.")64 help="Profile the memory usage using Dozer.")
54 parser.add_option("--prefix", dest="user_prefix",65 parser.add_option("--prefix", dest="user_prefix",
5566
=== added file 'loggerhead/htpasswd.py'
--- loggerhead/htpasswd.py 1970-01-01 00:00:00 +0000
+++ loggerhead/htpasswd.py 2010-02-08 20:46:16 +0000
@@ -0,0 +1,139 @@
1#!/usr/bin/python
2#
3# Originally from http://svn.edgewall.org/repos/trac/trunk/contrib/htpasswd.py
4# (revision 6651)
5#
6# According to http://svn.edgewall.org/repos/trac/trunk/contrib/README, this
7# is the license:
8#
9# Permission to use, copy, modify, and distribute this software and its
10# documentation for any purpose and without fee is hereby granted, provided
11# that the above copyright notice appears in all copies and that both the
12# copyright notice and this permission notice appear in supporting
13# documentation, and that the same name not be used in advertising or
14# publicity pertaining to distribution of the software without specific,
15# written prior permission. We make no representations about the
16# suitability this software for any purpose. It is provided "as is"
17# without express or implied warranty.
18"""Replacement for htpasswd"""
19# Original author: Eli Carter
20
21import os
22import sys
23import random
24from optparse import OptionParser
25
26# We need a crypt module, but Windows doesn't have one by default. Try to find
27# one, and tell the user if we can't.
28try:
29 import crypt
30except ImportError:
31 try:
32 import fcrypt as crypt
33 except ImportError:
34 sys.stderr.write("Cannot find a crypt module. "
35 "Possibly http://carey.geek.nz/code/python-fcrypt/\n")
36 sys.exit(1)
37
38
39def salt():
40 """Returns a string of 2 random letters"""
41 letters = 'abcdefghijklmnopqrstuvwxyz' \
42 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' \
43 '0123456789/.'
44 return random.choice(letters) + random.choice(letters)
45
46
47class HtpasswdFile:
48 """A class for manipulating htpasswd files."""
49
50 def __init__(self, filename, create=False):
51 self.entries = []
52 self.filename = filename
53 if not create:
54 if os.path.exists(self.filename):
55 self.load()
56 else:
57 raise Exception("%s does not exist" % self.filename)
58
59 def load(self):
60 """Read the htpasswd file into memory."""
61 lines = open(self.filename, 'r').readlines()
62 self.entries = []
63 for line in lines:
64 username, pwhash = line.split(':')
65 entry = [username, pwhash.rstrip()]
66 self.entries.append(entry)
67
68 def save(self):
69 """Write the htpasswd file to disk"""
70 open(self.filename, 'w').writelines(["%s:%s\n" % (entry[0], entry[1])
71 for entry in self.entries])
72
73 def update(self, username, password):
74 """Replace the entry for the given user, or add it if new."""
75 pwhash = crypt.crypt(password, salt())
76 matching_entries = [entry for entry in self.entries
77 if entry[0] == username]
78 if matching_entries:
79 matching_entries[0][1] = pwhash
80 else:
81 self.entries.append([username, pwhash])
82
83 def delete(self, username):
84 """Remove the entry for the given user."""
85 self.entries = [entry for entry in self.entries
86 if entry[0] != username]
87
88
89def main():
90 """%prog [-c] -b filename username password
91 Create or update an htpasswd file"""
92 # For now, we only care about the use cases that affect tests/functional.py
93 parser = OptionParser(usage=main.__doc__)
94 parser.add_option('-b', action='store_true', dest='batch', default=False,
95 help='Batch mode; password is passed on the command line IN THE '
96 'CLEAR.')
97 parser.add_option('-c', action='store_true', dest='create', default=False,
98 help='Create a new htpasswd file, overwriting any existing file.')
99 parser.add_option('-D', action='store_true', dest='delete_user',
100 default=False, help='Remove the given user from the password file.')
101
102 options, args = parser.parse_args()
103
104 def syntax_error(msg):
105 """Utility function for displaying fatal error messages with usage
106 help.
107 """
108 sys.stderr.write("Syntax error: " + msg)
109 sys.stderr.write(parser.get_usage())
110 sys.exit(1)
111
112 if not options.batch:
113 syntax_error("Only batch mode is supported\n")
114
115 # Non-option arguments
116 if len(args) < 2:
117 syntax_error("Insufficient number of arguments.\n")
118 filename, username = args[:2]
119 if options.delete_user:
120 if len(args) != 2:
121 syntax_error("Incorrect number of arguments.\n")
122 password = None
123 else:
124 if len(args) != 3:
125 syntax_error("Incorrect number of arguments.\n")
126 password = args[2]
127
128 passwdfile = HtpasswdFile(filename, create=options.create)
129
130 if options.delete_user:
131 passwdfile.delete(username)
132 else:
133 passwdfile.update(username, password)
134
135 passwdfile.save()
136
137
138if __name__ == '__main__':
139 main()
0140
=== modified file 'loggerhead/main.py'
--- loggerhead/main.py 2009-11-27 18:30:24 +0000
+++ loggerhead/main.py 2010-02-08 20:46:16 +0000
@@ -24,6 +24,8 @@
24from bzrlib.plugin import load_plugins24from bzrlib.plugin import load_plugins
2525
26from paste import httpserver26from paste import httpserver
27from paste.auth.basic import AuthBasicHandler
28from paste.auth.digest import AuthDigestHandler
27from paste.httpexceptions import HTTPExceptionHandler, HTTPInternalServerError29from paste.httpexceptions import HTTPExceptionHandler, HTTPInternalServerError
28from paste.translogger import TransLogger30from paste.translogger import TransLogger
2931
@@ -31,6 +33,7 @@
31from loggerhead.apps.transport import (33from loggerhead.apps.transport import (
32 BranchesFromTransportRoot, UserBranchesFromTransportRoot)34 BranchesFromTransportRoot, UserBranchesFromTransportRoot)
33from loggerhead.config import LoggerheadConfig35from loggerhead.config import LoggerheadConfig
36from loggerhead import htpasswd
34from loggerhead.util import Reloader37from loggerhead.util import Reloader
35from loggerhead.apps.error import ErrorHandlerApp38from loggerhead.apps.error import ErrorHandlerApp
3639
@@ -151,6 +154,39 @@
151 else:154 else:
152 host = config.get_option('user_host')155 host = config.get_option('user_host')
153156
157 if config.get_option('auth_realm'):
158 realm = config.get_option('auth_realm')
159 if config.get_option('digest_auth'):
160 path = config.get_option('digest_auth')
161 users = {}
162 # parse htdigest file
163 for line in open(path):
164 # format: $user:$realm:$hash
165 htuser, htrealm, hthash = line.strip().split(':', 2)
166 if htrealm not in users:
167 users[htrealm] = {}
168 users[htrealm][htuser] = hthash
169
170 def d_handler(environ, hrealm, huser):
171 return users.get(hrealm, {}).get(huser, False)
172 app = AuthDigestHandler(app, realm, d_handler)
173 elif config.get_option('basic_auth'):
174 path = config.get_option('basic_auth')
175 htp = htpasswd.HtpasswdFile(path)
176
177 def b_handler(environ, username, password):
178 result = [hthash for htuser, hthash in htp.entries
179 if htuser == username]
180 if len(result) == 0:
181 return False
182 hthash = result[0]
183 return htpasswd.crypt.crypt(password, hthash[:2]) == hthash
184 app = AuthBasicHandler(app, realm, b_handler)
185 elif config.get_option('basic_auth') or config.get_option('digest_auth'):
186 print 'Both the --basic-auth and --digest-auth flags require that', \
187 'the --auth-realm flag is set.'
188 sys.exit(1)
189
154 if not config.get_option('protocol'):190 if not config.get_option('protocol'):
155 protocol = 'http'191 protocol = 'http'
156 else:192 else:

Subscribers

People subscribed via source and target branches