| | 1 | # |
|---|
| | 2 | # Copyright (C) 2007-2008 Camptocamp |
|---|
| | 3 | # |
|---|
| | 4 | # This file is part of MapFish |
|---|
| | 5 | # |
|---|
| | 6 | # MapFish is free software: you can redistribute it and/or modify |
|---|
| | 7 | # it under the terms of the GNU Lesser General Public License as published by |
|---|
| | 8 | # the Free Software Foundation, either version 3 of the License, or |
|---|
| | 9 | # (at your option) any later version. |
|---|
| | 10 | # |
|---|
| | 11 | # MapFish is distributed in the hope that it will be useful, |
|---|
| | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| | 14 | # GNU Lesser General Public License for more details. |
|---|
| | 15 | # |
|---|
| | 16 | # You should have received a copy of the GNU Lesser General Public License |
|---|
| | 17 | # along with MapFish. If not, see <http://www.gnu.org/licenses/>. |
|---|
| | 18 | # |
|---|
| | 19 | |
|---|
| | 20 | import logging |
|---|
| | 21 | |
|---|
| | 22 | from pylons import request, response |
|---|
| | 23 | from pylons.controllers import WSGIController |
|---|
| | 24 | from pylons.controllers.util import abort |
|---|
| | 25 | from routes.util import url_for |
|---|
| | 26 | from authkit.authorize.pylons_adaptors import authorized |
|---|
| | 27 | from authkit.authorize import NotAuthorizedError |
|---|
| | 28 | import urllib2 |
|---|
| | 29 | from urllib import urlencode |
|---|
| | 30 | import urlparse |
|---|
| | 31 | |
|---|
| | 32 | def add_routes(map, url, controller): |
|---|
| | 33 | """ |
|---|
| | 34 | Add the pylons routes for a proxy |
|---|
| | 35 | """ |
|---|
| | 36 | map.connect(url+"*more", controller = controller, action = 'get', conditions = dict(method = ['GET'])) |
|---|
| | 37 | |
|---|
| | 38 | log = logging.getLogger(__name__) |
|---|
| | 39 | |
|---|
| | 40 | |
|---|
| | 41 | class AuthProxyController(WSGIController): |
|---|
| | 42 | """Implements a proxy (HTTP GET only) with possibilities to authorize only |
|---|
| | 43 | certain parameter values in function of AuthKit permissions. |
|---|
| | 44 | |
|---|
| | 45 | For each URL parameter to check, child classes can define class variables |
|---|
| | 46 | named "PERMISSION_{paramName.upper()}" containing a dictionary with the |
|---|
| | 47 | parameter values (lowercase) as keys and a AuthKit permission as value. |
|---|
| | 48 | Keys can be regular expressions as well. A default permission can be defined |
|---|
| | 49 | with the key "DEFAULT" (uppercase). If a parameter value is not defined or |
|---|
| | 50 | is None, authorisation is granted. If no class variable is found for a |
|---|
| | 51 | parameters, authorisation is granted. |
|---|
| | 52 | |
|---|
| | 53 | This proxy supports additional URL information. The permission dictionary |
|---|
| | 54 | to control that is "PERMISSION__". |
|---|
| | 55 | |
|---|
| | 56 | If the "standard" way to check the permission is not enough for you, you can |
|---|
| | 57 | always override the _check method and add your tests in it. |
|---|
| | 58 | |
|---|
| | 59 | To use it, add a controller like that to your project: |
|---|
| | 60 | from mapfish.controllers.auth_proxy import AuthProxyController, Deny |
|---|
| | 61 | from authkit.permissions import HasAuthKitRole, ValidAuthKitUser |
|---|
| | 62 | import re |
|---|
| | 63 | |
|---|
| | 64 | class WmsAuthProxyController(AuthProxyController): |
|---|
| | 65 | # Base URL to redirect to |
|---|
| | 66 | URL = "http://www.example.com" |
|---|
| | 67 | |
|---|
| | 68 | # Permissions on what can be added to the URL |
|---|
| | 69 | PERMISSIONS__ = { |
|---|
| | 70 | '/tilecache': None, |
|---|
| | 71 | 'DEFAULT': Deny() |
|---|
| | 72 | } |
|---|
| | 73 | |
|---|
| | 74 | # Permission on what layer can be read by whom |
|---|
| | 75 | PERMISSIONS_LAYERS = { |
|---|
| | 76 | 'background': None, |
|---|
| | 77 | 'stuff': HasAuthKitRole('view'), |
|---|
| | 78 | re.compile('^secret\d+$'): HasAuthKitRole('god'), |
|---|
| | 79 | 'DEFAULT': Deny() |
|---|
| | 80 | } |
|---|
| | 81 | |
|---|
| | 82 | Then, add this to your routings.py: |
|---|
| | 83 | from mapfish.controllers import auth_proxy |
|---|
| | 84 | auth_proxy.addRoutes(map, 'wmsProxy', 'wms_auth_proxy') |
|---|
| | 85 | |
|---|
| | 86 | """ |
|---|
| | 87 | |
|---|
| | 88 | def _has_checks(self, field): |
|---|
| | 89 | return hasattr(self, "PERMISSIONS_"+field.upper()) |
|---|
| | 90 | |
|---|
| | 91 | def _get_role(self, field, value): |
|---|
| | 92 | dico = getattr(self, "PERMISSIONS_"+field.upper()) |
|---|
| | 93 | |
|---|
| | 94 | # try direct access |
|---|
| | 95 | role = dico.get(value.lower(), 0) |
|---|
| | 96 | if role != 0: |
|---|
| | 97 | return role |
|---|
| | 98 | |
|---|
| | 99 | # try with regular expression |
|---|
| | 100 | for key in dico: |
|---|
| | 101 | if hasattr(key, 'match'): |
|---|
| | 102 | if key.match(value): |
|---|
| | 103 | return dico[key] |
|---|
| | 104 | |
|---|
| | 105 | # try the DEFAULT value |
|---|
| | 106 | return dico.get("DEFAULT", None) |
|---|
| | 107 | |
|---|
| | 108 | def _get_url(self): |
|---|
| | 109 | return self.URL |
|---|
| | 110 | |
|---|
| | 111 | def get(self, more): |
|---|
| | 112 | """Proxy action |
|---|
| | 113 | """ |
|---|
| | 114 | check_result = self._check(more) |
|---|
| | 115 | if check_result == "": |
|---|
| | 116 | return self._proxy(more) |
|---|
| | 117 | else: |
|---|
| | 118 | response.status=406 |
|---|
| | 119 | return check_result |
|---|
| | 120 | |
|---|
| | 121 | def _check(self, more): |
|---|
| | 122 | """Do the actual checking |
|---|
| | 123 | """ |
|---|
| | 124 | result = "" |
|---|
| | 125 | if self._has_checks("_"): |
|---|
| | 126 | perm = self._get_role("_", more) |
|---|
| | 127 | if perm != None and not authorized(perm): |
|---|
| | 128 | msg = "Not allowed to have the URL with value '%s' as user '%s'<br/>\n" % ( |
|---|
| | 129 | more, |
|---|
| | 130 | request.environ.get("REMOTE_USER", "anonymous") |
|---|
| | 131 | ) |
|---|
| | 132 | result += msg |
|---|
| | 133 | |
|---|
| | 134 | #check the parameters for special rights |
|---|
| | 135 | for param in request.params: |
|---|
| | 136 | if self._has_checks(param): |
|---|
| | 137 | paramValues = request.params[param].split(",") |
|---|
| | 138 | for paramValue in paramValues: |
|---|
| | 139 | paramValue = paramValue.strip() |
|---|
| | 140 | perm = self._get_role(param, paramValue) |
|---|
| | 141 | if perm!=None and not authorized(perm): |
|---|
| | 142 | msg = "Not allowed to have the parameter '%s' with value '%s' as user '%s'<br/>\n" % ( |
|---|
| | 143 | param, |
|---|
| | 144 | paramValue, |
|---|
| | 145 | request.environ.get("REMOTE_USER", "anonymous") |
|---|
| | 146 | ) |
|---|
| | 147 | result += msg |
|---|
| | 148 | return result |
|---|
| | 149 | |
|---|
| | 150 | def _proxy(self, more): |
|---|
| | 151 | """Do the actual action of proxying the call. |
|---|
| | 152 | """ |
|---|
| | 153 | query = urlencode(request.params) |
|---|
| | 154 | fullUrl = self._get_url() + more; |
|---|
| | 155 | if query: |
|---|
| | 156 | if not fullUrl.endswith("?"): |
|---|
| | 157 | fullUrl += "?" |
|---|
| | 158 | fullUrl += query |
|---|
| | 159 | |
|---|
| | 160 | #build the request with its headers |
|---|
| | 161 | req = urllib2.Request(url=fullUrl) |
|---|
| | 162 | for header in request.headers: |
|---|
| | 163 | if header.lower()=="host": |
|---|
| | 164 | req.add_header(header, urlparse.urlparse(self._get_url())[1]) |
|---|
| | 165 | else: |
|---|
| | 166 | req.add_header(header, request.headers[header]) |
|---|
| | 167 | res = urllib2.urlopen(req) |
|---|
| | 168 | |
|---|
| | 169 | #add response headers |
|---|
| | 170 | i = res.info() |
|---|
| | 171 | response.status = res.code |
|---|
| | 172 | gotContentLength = False |
|---|
| | 173 | for header in i: |
|---|
| | 174 | response.headers[header] =i[header] |
|---|
| | 175 | if header.lower() == "content-length": |
|---|
| | 176 | gotContentLength = True |
|---|
| | 177 | |
|---|
| | 178 | #return the result |
|---|
| | 179 | result = res.read(); |
|---|
| | 180 | res.close(); |
|---|
| | 181 | |
|---|
| | 182 | if not gotContentLength: |
|---|
| | 183 | response.headers['content-length'] = str(len(result)) |
|---|
| | 184 | return result; |
|---|
| | 185 | |
|---|
| | 186 | |
|---|
| | 187 | class Deny: |
|---|
| | 188 | """A special AuthKit permission that always deny access |
|---|
| | 189 | """ |
|---|
| | 190 | def check(self, app, environ, start_response): |
|---|
| | 191 | raise NotAuthorizedError("Not authorized at all") |