paulb@646 | 1 | #!/usr/bin/env python |
paulb@646 | 2 | |
paulb@646 | 3 | """ |
paulb@651 | 4 | OpenID login resources for XSLForms applications. These resources use "root" |
paulb@646 | 5 | attributes on transaction objects, and therefore should be defined within the |
paulb@646 | 6 | appropriate resources in site maps. |
paulb@646 | 7 | |
paulb@646 | 8 | Copyright (C) 2006, 2007 Paul Boddie <paul@boddie.org.uk> |
paulb@646 | 9 | |
paulb@646 | 10 | This program is free software; you can redistribute it and/or modify it under |
paulb@646 | 11 | the terms of the GNU Lesser General Public License as published by the Free |
paulb@646 | 12 | Software Foundation; either version 3 of the License, or (at your option) any |
paulb@646 | 13 | later version. |
paulb@646 | 14 | |
paulb@646 | 15 | This program is distributed in the hope that it will be useful, but WITHOUT |
paulb@646 | 16 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
paulb@646 | 17 | FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
paulb@646 | 18 | details. |
paulb@646 | 19 | |
paulb@646 | 20 | You should have received a copy of the GNU Lesser General Public License along |
paulb@646 | 21 | with this program. If not, see <http://www.gnu.org/licenses/>. |
paulb@646 | 22 | """ |
paulb@646 | 23 | |
paulb@646 | 24 | from WebStack.Generic import ContentType, EndOfResponse |
paulb@651 | 25 | from WebStack.Resources.OpenIDLogin import OpenIDLoginUtils, Authenticator |
paulb@646 | 26 | from XSLForms.Resources.WebResources import XSLFormsResource |
paulb@646 | 27 | |
paulb@646 | 28 | class OpenIDLoginResource(XSLFormsResource, OpenIDLoginUtils): |
paulb@646 | 29 | |
paulb@646 | 30 | """ |
paulb@646 | 31 | A login screen resource which should be modified or subclassed to define the |
paulb@646 | 32 | following attributes: |
paulb@646 | 33 | |
paulb@646 | 34 | * resource_dir |
paulb@646 | 35 | * template_resources - including a "login" entry for the login screen and |
paulb@646 | 36 | a "success" entry for a screen indicating a |
paulb@646 | 37 | successful login (used when redirects are not in |
paulb@646 | 38 | use) |
paulb@646 | 39 | * document_resources - including a "translations" entry |
paulb@646 | 40 | |
paulb@646 | 41 | The latter attribute is optional. |
paulb@646 | 42 | |
paulb@646 | 43 | The login template must define a "login" action, and provide a document |
paulb@646 | 44 | structure where the login credentials can be found through this class's |
paulb@646 | 45 | 'path_to_login_element' attribute (which can be overridden or modified). |
paulb@646 | 46 | Such a structure would be as follows for the default configuration: |
paulb@646 | 47 | |
paulb@646 | 48 | <login username="..." password="..."/> |
paulb@646 | 49 | |
paulb@646 | 50 | The success template must provide a document structure where the location of |
paulb@646 | 51 | the application can be found through this class's 'path_to_success_element' |
paulb@646 | 52 | attribute (which can be overridden or modified). Such a structure would be |
paulb@646 | 53 | as follows for the default configuration: |
paulb@646 | 54 | |
paulb@646 | 55 | <success location="..."/> |
paulb@646 | 56 | """ |
paulb@646 | 57 | |
paulb@646 | 58 | path_to_login_element = "/login" |
paulb@646 | 59 | path_to_success_element = "/success" |
paulb@646 | 60 | |
paulb@646 | 61 | def __init__(self, app_url, authenticator, associations=None, use_redirect=1): |
paulb@646 | 62 | |
paulb@646 | 63 | """ |
paulb@646 | 64 | Initialise the resource with an 'app_url' and an 'authenticator'. |
paulb@646 | 65 | |
paulb@646 | 66 | The optional 'associations' is a mapping from association handles to |
paulb@646 | 67 | secret keys. |
paulb@646 | 68 | |
paulb@646 | 69 | If the optional 'use_redirect' flag is set to a false value (which is |
paulb@646 | 70 | not the default), a confirmation screen is given instead of immediately |
paulb@646 | 71 | redirecting the user back to the original application. |
paulb@646 | 72 | |
paulb@646 | 73 | To get the root of the application, this resource needs an attribute on |
paulb@646 | 74 | the transaction called "root". |
paulb@646 | 75 | """ |
paulb@646 | 76 | |
paulb@651 | 77 | OpenIDLoginUtils.__init__(self, app_url, authenticator, associations, use_redirect) |
paulb@646 | 78 | |
paulb@646 | 79 | def select_activity(self, trans, form): |
paulb@646 | 80 | form.set_activity("login") |
paulb@646 | 81 | |
paulb@646 | 82 | def respond_to_input(self, trans, form): |
paulb@646 | 83 | parameters = form.get_parameters() |
paulb@646 | 84 | |
paulb@646 | 85 | # Test for login. |
paulb@646 | 86 | |
paulb@646 | 87 | if parameters.has_key("login"): |
paulb@646 | 88 | self.check_login(trans, form) |
paulb@646 | 89 | |
paulb@646 | 90 | # Check for an OpenID signature verification request. |
paulb@646 | 91 | |
paulb@646 | 92 | elif parameters.get("openid.mode", [None])[0] == "check_authentication": |
paulb@646 | 93 | self.check_authentication(trans, trans.get_fields()) |
paulb@646 | 94 | |
paulb@646 | 95 | # NOTE: Permit association requests here. |
paulb@646 | 96 | # Otherwise, show the login form. |
paulb@646 | 97 | |
paulb@646 | 98 | else: |
paulb@646 | 99 | self.show_login(trans, form) |
paulb@646 | 100 | |
paulb@646 | 101 | # Methods called by the OpenID logic. |
paulb@646 | 102 | |
paulb@646 | 103 | def check_login(self, trans, form): |
paulb@646 | 104 | doc = form.get_document() |
paulb@646 | 105 | parameters = form.get_parameters() |
paulb@646 | 106 | |
paulb@646 | 107 | logelem = doc.xpath(self.path_to_login_element)[0] |
paulb@646 | 108 | return_to = logelem.getAttribute("return_to") or parameters.get("openid.return_to", [""])[0] |
paulb@646 | 109 | claimed_id = logelem.getAttribute("claimed_id") or parameters.get("openid.claimed_id", [""])[0] |
paulb@646 | 110 | local_id = logelem.getAttribute("identity") or parameters.get("openid.identity", [""])[0] |
paulb@646 | 111 | |
paulb@646 | 112 | username = logelem.getAttribute("username") |
paulb@646 | 113 | password = logelem.getAttribute("password") |
paulb@646 | 114 | |
paulb@646 | 115 | # If successful, switch to the success template and redirect. |
paulb@646 | 116 | # NOTE: Permit flexibility in the credentials. |
paulb@646 | 117 | |
paulb@646 | 118 | if self.authenticator.authenticate(trans, (local_id, username), password): |
paulb@646 | 119 | endpoint = self.app_url + trans.get_path_without_query() |
paulb@646 | 120 | self.redirect_to_application(trans, form, claimed_id, local_id, username, return_to, endpoint) |
paulb@646 | 121 | else: |
paulb@646 | 122 | error = doc.createElement("error") |
paulb@646 | 123 | logelem.appendChild(error) |
paulb@646 | 124 | error.setAttribute("message", "Username or password not valid") |
paulb@646 | 125 | self.show_login(trans, form) |
paulb@646 | 126 | |
paulb@646 | 127 | def redirect_to_application(self, trans, form, claimed_id, local_id, username, return_to, endpoint): |
paulb@646 | 128 | |
paulb@646 | 129 | """ |
paulb@646 | 130 | Redirect the client using 'trans', 'claimed_id', 'local_id', 'username' |
paulb@646 | 131 | and the given 'return_to' and 'endpoint' details. |
paulb@646 | 132 | """ |
paulb@646 | 133 | |
paulb@646 | 134 | fields = self.get_openid_fields(trans, claimed_id, local_id, username, return_to, endpoint) |
paulb@646 | 135 | url = self.get_openid_url(trans, fields) |
paulb@646 | 136 | |
paulb@646 | 137 | # Show the success page anyway. |
paulb@646 | 138 | # Offer a POST-based form for redirection. |
paulb@646 | 139 | |
paulb@646 | 140 | self.show_success(trans, form, fields) |
paulb@646 | 141 | if self.use_redirect: |
paulb@646 | 142 | trans.redirect(url) |
paulb@646 | 143 | |
paulb@646 | 144 | def show_login(self, trans, form): |
paulb@646 | 145 | |
paulb@646 | 146 | """ |
paulb@646 | 147 | Writes a login screen using the transaction 'trans' and 'form', |
paulb@646 | 148 | including details of the 'return_to' URL which the client was attempting |
paulb@646 | 149 | to access, along with the 'claimed_id' and 'local_id'. |
paulb@646 | 150 | """ |
paulb@646 | 151 | |
paulb@646 | 152 | doc = form.get_document() |
paulb@646 | 153 | parameters = form.get_parameters() |
paulb@646 | 154 | |
paulb@646 | 155 | logelem = doc.xpath(self.path_to_login_element)[0] |
paulb@646 | 156 | return_to = logelem.getAttribute("return_to") or parameters.get("openid.return_to", [""])[0] |
paulb@646 | 157 | claimed_id = logelem.getAttribute("claimed_id") or parameters.get("openid.claimed_id", [""])[0] |
paulb@646 | 158 | local_id = logelem.getAttribute("identity") or parameters.get("openid.identity", [""])[0] |
paulb@646 | 159 | |
paulb@646 | 160 | logelem = doc.xpath(self.path_to_login_element)[0] |
paulb@646 | 161 | logelem.setAttribute("return_to", return_to) |
paulb@646 | 162 | logelem.setAttribute("claimed_id", claimed_id) |
paulb@646 | 163 | logelem.setAttribute("identity", local_id) |
paulb@646 | 164 | |
paulb@646 | 165 | def show_success(self, trans, form, fields): |
paulb@646 | 166 | |
paulb@646 | 167 | """ |
paulb@646 | 168 | Writes a success screen using the transaction 'trans' and 'form', using |
paulb@646 | 169 | a dictionary of 'fields' providing details of the transaction. |
paulb@646 | 170 | """ |
paulb@646 | 171 | |
paulb@646 | 172 | # Switch to the success activity. |
paulb@646 | 173 | |
paulb@646 | 174 | form.set_activity("success") |
paulb@646 | 175 | doc = form.new_instance("success") |
paulb@646 | 176 | successelem = doc.xpath(self.path_to_success_element)[0] |
paulb@646 | 177 | successelem.setAttribute("location", fields["openid.return_to"][0]) |
paulb@646 | 178 | |
paulb@646 | 179 | # Add OpenID fields. |
paulb@646 | 180 | |
paulb@646 | 181 | for name, values in fields.items(): |
paulb@646 | 182 | field = doc.createElement("field") |
paulb@646 | 183 | field.setAttribute("name", name) |
paulb@646 | 184 | field.setAttribute("value", values[0]) |
paulb@646 | 185 | successelem.appendChild(field) |
paulb@646 | 186 | |
paulb@646 | 187 | form.set_document(doc) |
paulb@646 | 188 | |
paulb@646 | 189 | # Output preparation. |
paulb@646 | 190 | |
paulb@646 | 191 | def create_output(self, trans, form): |
paulb@646 | 192 | attributes = trans.get_attributes() |
paulb@646 | 193 | |
paulb@646 | 194 | stylesheet_parameters = {} |
paulb@646 | 195 | references = {} |
paulb@646 | 196 | |
paulb@646 | 197 | # Set up translations. |
paulb@646 | 198 | |
paulb@646 | 199 | if self.document_resources.has_key("translations"): |
paulb@646 | 200 | translations_xml = self.prepare_document("translations") |
paulb@646 | 201 | |
paulb@646 | 202 | try: |
paulb@646 | 203 | language = trans.get_content_languages()[0] |
paulb@646 | 204 | except IndexError: |
paulb@646 | 205 | language = "en" |
paulb@646 | 206 | |
paulb@646 | 207 | stylesheet_parameters["locale"] = language |
paulb@646 | 208 | references["translations"] = translations_xml |
paulb@646 | 209 | |
paulb@646 | 210 | # Complete the response. |
paulb@646 | 211 | |
paulb@646 | 212 | stylesheet_parameters["root"] = attributes["root"] |
paulb@646 | 213 | XSLFormsResource.create_output(self, trans, form, stylesheet_parameters=stylesheet_parameters, references=references) |
paulb@646 | 214 | |
paulb@646 | 215 | # vim: tabstop=4 expandtab shiftwidth=4 |