[Pypi-checkins] r968 - in trunk/pypi: . templates tools
richard
python-checkins at python.org
Mon Sep 5 03:36:47 CEST 2011
Author: richard
Date: Mon Sep 5 03:36:47 2011
New Revision: 968
Added:
trunk/pypi/templates/openid_decide.pt
trunk/pypi/templates/openid_notloggedin.pt
trunk/pypi/tools/sql-migrate-20110905.sql
Modified:
trunk/pypi/pkgbase_schema.sql
trunk/pypi/store.py
trunk/pypi/webui.py
Log:
openid provider support; thanks Mark Rees
Modified: trunk/pypi/pkgbase_schema.sql
==============================================================================
--- trunk/pypi/pkgbase_schema.sql (original)
+++ trunk/pypi/pkgbase_schema.sql Mon Sep 5 03:36:47 2011
@@ -285,5 +285,12 @@
PRIMARY KEY(name)
);
+CREATE TABLE openid_whitelist
+(
+ "name" text NOT NULL,
+ trust_root text NOT null,
+ created timestamp without time zone,
+ CONSTRAINT openid_whitelist__pkey PRIMARY KEY (name, trust_root)
+);
commit;
Modified: trunk/pypi/store.py
==============================================================================
--- trunk/pypi/store.py (original)
+++ trunk/pypi/store.py Mon Sep 5 03:36:47 2011
@@ -1932,6 +1932,28 @@
cursor = self.get_cursor()
safe_execute(cursor, 'delete from openids where id=%s', (openid,))
+ def set_openid_trustedroot(self, username, trusted_root):
+ now = datetime.datetime.now()
+ cursor = self.get_cursor()
+ safe_execute(cursor, '''select * from openid_whitelist
+ where name=%s and trust_root=%s''',
+ (username, trusted_root))
+ if not cursor.fetchone():
+ safe_execute(cursor, '''insert into openid_whitelist(
+ name, trust_root, created) values(%s,%s,%s)''',
+ (username, trusted_root, now))
+
+ def check_openid_trustedroot(self, username, trusted_root):
+ """Check trusted_root is in user's whitelist"""
+ cursor = self.get_cursor()
+ safe_execute(cursor, '''select * from openid_whitelist
+ where name=%s and trust_root=%s''',
+ (username, trusted_root))
+ if cursor.fetchone():
+ return True
+ else:
+ return False
+
def log_keyrotate(self):
cursor = self.get_cursor()
date = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime())
Added: trunk/pypi/templates/openid_decide.pt
==============================================================================
--- (empty file)
+++ trunk/pypi/templates/openid_decide.pt Mon Sep 5 03:36:47 2011
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ metal:use-macro="standard_template/macros/page">
+ <metal:fill fill-slot="body">
+
+ <form tal:attributes="action data/url_path" method="POST">
+ <p>The site <span tal:replace="data/trust_root" /> is asking for confirmation that
+ <span tal:replace="data/pending_id"/> is your identity id.</p>
+ <input type="submit" name="allow" value="Allow"/>
+ <input type="submit" name="allow_always" value="Allow Always"/>
+ <input type="submit" name="no_thanks" value="No Thanks"/>
+ <div tal:repeat="item python:data['orequest'].items()" tal:omit-tag="">
+ <input type="hidden" tal:attributes="name python:item[0];value python:item[1]"/>
+ </div>
+ </form>
+
+ </metal:fill>
+</html>
\ No newline at end of file
Added: trunk/pypi/templates/openid_notloggedin.pt
==============================================================================
--- (empty file)
+++ trunk/pypi/templates/openid_notloggedin.pt Mon Sep 5 03:36:47 2011
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ metal:use-macro="standard_template/macros/page">
+ <metal:fill fill-slot="body">
+
+ <p>You need to login to this site before trying OpenId authentication again.</p>
+
+ </metal:fill>
+</html>
\ No newline at end of file
Added: trunk/pypi/tools/sql-migrate-20110905.sql
==============================================================================
--- (empty file)
+++ trunk/pypi/tools/sql-migrate-20110905.sql Mon Sep 5 03:36:47 2011
@@ -0,0 +1,8 @@
+CREATE TABLE openid_whitelist
+(
+ "name" text NOT NULL,
+ trust_root text NOT null,
+ created timestamp without time zone,
+ CONSTRAINT openid_whitelist__pkey PRIMARY KEY (name, trust_root)
+);
+ALTER TABLE openid_whitelist OWNER TO pgs_rw;
Modified: trunk/pypi/webui.py
==============================================================================
--- trunk/pypi/webui.py (original)
+++ trunk/pypi/webui.py Mon Sep 5 03:36:47 2011
@@ -26,6 +26,12 @@
from M2Crypto import EVP, DSA
urllib.URLopener.open_https = orig
+# OpenId provider imports
+OPENID_FILESTORE = '/tmp/openid-filestore'
+
+from openid.store.filestore import FileOpenIDStore
+from openid.server import server as OpenIDServer
+
# local imports
import store, config, versionpredicate, verify_filetype, rpc
import MailingLogger, openid2rp, gae
@@ -50,10 +56,14 @@
pass
class Redirect(Exception):
pass
+class RedirectFound(Exception):# 302
+ pass
class RedirectTemporary(Exception): # 307
pass
class FormError(Exception):
pass
+class OpenIDError(Exception):
+ pass
class MultipleReleases(Exception):
def __init__(self, releases):
@@ -208,6 +218,8 @@
self.loggedin = False # was a valid cookie sent?
self.usercookie = None
self.failed = None # error message if initialization already produced a failure
+ self.op_endpoint = "%s?:action=openid_endpoint" % (self.config.url,)
+ self.oid_server = OpenIDServer.Server(FileOpenIDStore(OPENID_FILESTORE), op_endpoint=self.op_endpoint)
# XMLRPC request or not?
if self.env.get('CONTENT_TYPE') != 'text/xml':
@@ -275,6 +287,10 @@
self.handler.send_response(301, 'Moved Permanently')
self.handler.send_header('Location', e.args[0])
self.handler.end_headers()
+ except RedirectFound, e:
+ self.handler.send_response(302, 'Found')
+ self.handler.send_header('Location', e.args[0])
+ self.handler.end_headers()
except RedirectTemporary, e:
# ask browser not to cache this redirect
self.handler.send_response(307, 'Temporary Redirect')
@@ -283,6 +299,9 @@
except FormError, message:
message = str(message)
self.fail(message, code=400, heading='Error processing form')
+ except OpenIDError, message:
+ message = str(message)
+ self.fail(message, code=400, heading='Error processing OpenID request')
except IOError, error:
# ignore broken pipe errors (client vanished on us)
if error.errno != 32: raise
@@ -543,7 +562,8 @@
password_reset role role_form list_classifiers login logout files
file_upload show_md5 doc_upload claim openid openid_return dropid
clear_auth addkey delkey lasthour json gae_file about delete_user
- rss_regen'''.split():
+ rss_regen openid_discovery openid_endpoint openid_decide_post
+ openid_user'''.split():
getattr(self, action)()
else:
#raise NotFound, 'Unknown action %s' % action
@@ -1328,7 +1348,7 @@
'platform bugtrack_url').split()
release = {'description_html': ''}
- bugtrack_url =''
+ bugtrack_url =''
for column in columns:
value = info[column]
if not info[column]: continue
@@ -1351,8 +1371,8 @@
elif column.startswith('cheesecake_'):
column = column[:-3]
value = self.store.get_cheesecake_index(int(value))
- elif column == 'bugtrack_url':
- bugtrack_url = value
+ elif column == 'bugtrack_url':
+ bugtrack_url = value
value = info[column]
release[column] = value
@@ -2918,3 +2938,164 @@
if p.returncode != 0:
raise FormError, "Key processing failed. Please contact the administrator. Detail: "+stdout
+ def openid_discovery(self):
+ """Return an XRDS document containing an OpenID provider endpoint URL."""
+ payload = '''<xrds:XRDS
+ xmlns:xrds="xri://$xrds"
+ xmlns="xri://$xrd*($v*2.0)">
+ <XRD>
+ <Service priority="0">
+ <Type>http://specs.openid.net/auth/2.0/server</Type>
+ <Type>http://specs.openid.net/auth/2.0/signon</Type>
+ <URI>%s</URI>
+ </Service>
+ </XRD>
+ </xrds:XRDS>
+ ''' % (self.config.url+'?:action=openid_endpoint')
+ self.handler.send_response(200)
+ self.handler.send_header("Content-type", 'application/xrds+xml')
+ self.handler.send_header("Content-length", str(len(payload)))
+ self.handler.end_headers()
+ self.handler.wfile.write(payload)
+
+ def openid_user(self):
+ """Return an XRDS document containing an OpenID provider endpoint URL."""
+ payload = '''<xrds:XRDS
+ xmlns:xrds="xri://$xrds"
+ xmlns="xri://$xrd*($v*2.0)">
+ <XRD>
+ <Service priority="0">
+ <Type>http://specs.openid.net/auth/2.0/signon</Type>
+ <URI>%s</URI>
+ </Service>
+ </XRD>
+ </xrds:XRDS>
+ ''' % (self.config.url+'?:action=openid_endpoint')
+ self.handler.send_response(200)
+ self.handler.send_header("Content-type", 'application/xrds+xml')
+ self.handler.send_header("Content-length", str(len(payload)))
+ self.handler.end_headers()
+ self.handler.wfile.write(payload)
+
+ def openid_endpoint(self):
+ """Handle OpenID requests"""
+ orequest = self.oid_server.decodeRequest(self.form)
+ if not orequest or orequest is None:
+ payload='''This is an OpenID server'''
+ self.handler.send_response(200)
+ self.handler.send_header("Content-type", 'text/plain')
+ self.handler.send_header("Content-length", str(len(payload)))
+ self.handler.end_headers()
+ self.handler.wfile.write(payload)
+ return
+ if orequest.mode in ['checkid_immediate', 'checkid_setup']:
+ if self.openid_is_authorized(orequest):
+ return self.openid_response(orequest.answer(True))
+ elif orequest.immediate:
+ return self.openid_response(orequest.answer(False))
+ else:
+ self.openid_decide_page(orequest)
+ elif orequest.mode in ['associate', 'check_authentication']:
+ self.openid_response(self.oid_server.handleRequest(orequest))
+ else:
+ raise OpenIDError, "Unknown mode: %s" % orequest.mode
+
+
+ def openid_decide_page(self, orequest):
+ """
+ The page that asks the user if they really want to trust this trust_root
+ If they are NOT logged intp PyPI, show the landing page so the user
+ understands why it has failed and they need to login to PyPI before
+ attempting again. This is done rather than presenting PyPI login page
+ to reduce chance of phishing.
+ """
+ if not self.authenticated:
+ self.write_template('openid_notloggedin.pt',
+ title="OpenId landing page")
+ return
+
+ if orequest.identity == "http://specs.openid.net/auth/2.0/identifier_select":
+ pending_id = self.openid_user_url()
+ else:
+ pending_id = orequest.identity
+
+ orequest_args=orequest.message.toPostArgs()
+ del orequest_args[':action']
+ # They are logged in - ask if they want to trust this root
+ self.write_template('openid_decide.pt', title="Trust this site?",
+ url_path="%s/?:action=openid_decide_post" % self.config.url,
+ orequest=orequest_args,
+ mode=orequest.mode,
+ identity=self.username,
+ return_to=orequest.return_to,
+ trust_root=orequest.trust_root,
+ pending_id = pending_id)
+
+ def openid_decide_post(self):
+ """Handle POST request from decide form"""
+ if self.env['REQUEST_METHOD'] != "POST":
+ raise OpenIDError, "OpenID request must be a POST"
+
+ from openid.message import Message
+ message = Message.fromPostArgs(self.form)
+ orequest = OpenIDServer.CheckIDRequest.fromMessage(message, self.oid_server.op_endpoint)
+
+ if self.form.has_key('allow'):
+ answer = orequest.answer(True,
+ identity=self.openid_user_url())
+ return self.openid_response(answer)
+ elif self.form.has_key('allow_always'):
+ answer = orequest.answer(True,
+ identity=self.openid_user_url())
+ self.store.set_openid_trustedroot(self.username, orequest.trust_root)
+ self.store.commit()
+ return self.openid_response(answer)
+ elif self.form.has_key('no_thanks'):
+ answer = orequest.answer(False)
+ return self.openid_response(answer)
+ else:
+ raise OpenIDError, "OpenID post request failure"
+
+ def openid_response(self, oresponse):
+ """Convert a webresponse from the OpenID library into a
+ WebUI http response"""
+ webresponse = self.oid_server.encodeResponse(oresponse)
+ if webresponse.code == 301:
+ raise Redirect, str(webresponse.headers['location'])
+ elif webresponse.code == 302:
+ raise RedirectFound, str(webresponse.headers['location'])
+
+ self.handler.send_response(webresponse.code)
+ for key, value in webresponse.headers.items():
+ self.handler.send_header(key, str(value))
+ self.handler.end_headers()
+ self.handler.wfile.write(webresponse.body)
+
+ def openid_is_authorized(self, orequest):
+ """
+ This should check that they own the given identity,
+ and that the trust_root is in their whitelist of trusted sites.
+ """
+ identity = orequest.identity
+ if not self.authenticated:
+ return False
+ if identity == 'http://specs.openid.net/auth/2.0/identifier_select':
+ return False
+ qs = urlparse.urlparse(identity).query
+ if urlparse.parse_qs(qs).get("username",[None])[0] == self.username:
+ if self.store.check_openid_trustedroot(self.username,
+ orequest.trust_root):
+ return True
+ else:
+ return False
+ # identity is not owned by user so decline the request
+ answer = orequest.answer(False)
+ self.openid_response(answer)
+
+ def openid_user_url(self):
+ if self.authenticated:
+ return "%s?:action=openid_user&username=%s" % (self.config.url,
+ self.username)
+ else:
+ return None
+
More information about the Pypi-checkins
mailing list