Commit | Line | Data |
---|---|---|
a062149e JW |
1 | # GNU MediaGoblin -- federated, autonomous media hosting |
2 | # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. | |
3 | # | |
4 | # This program is free software: you can redistribute it and/or modify | |
5 | # it under the terms of the GNU Affero General Public License as published by | |
6 | # the Free Software Foundation, either version 3 of the License, or | |
7 | # (at your option) any later version. | |
8 | # | |
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU Affero General Public License for more details. | |
13 | # | |
14 | # You should have received a copy of the GNU Affero General Public License | |
15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
17 | import logging | |
42c83752 | 18 | import json |
a062149e JW |
19 | |
20 | from functools import wraps | |
62d14bf5 | 21 | from webob import Response |
c92aa0d0 | 22 | from urlparse import urljoin |
62d14bf5 | 23 | from werkzeug.exceptions import Forbidden |
a062149e | 24 | |
42c83752 | 25 | from mediagoblin import mg_globals |
a062149e | 26 | from mediagoblin.tools.pluginapi import PluginManager |
42c83752 | 27 | from mediagoblin.storage.filestorage import BasicFileStorage |
a062149e JW |
28 | |
29 | _log = logging.getLogger(__name__) | |
30 | ||
31 | ||
42c83752 JW |
32 | class Auth(object): |
33 | ''' | |
34 | An object with two significant methods, 'trigger' and 'run'. | |
35 | ||
36 | Using a similar object to this, plugins can register specific | |
37 | authentication logic, for example the GET param 'access_token' for OAuth. | |
38 | ||
39 | - trigger: Analyze the 'request' argument, return True if you think you | |
40 | can handle the request, otherwise return False | |
41 | - run: The authentication logic, set the request.user object to the user | |
42 | you intend to authenticate and return True, otherwise return False. | |
43 | ||
44 | If run() returns False, an HTTP 403 Forbidden error will be shown. | |
45 | ||
46 | You may also display custom errors, just raise them within the run() | |
47 | method. | |
48 | ''' | |
49 | def trigger(self, request): | |
50 | raise NotImplemented() | |
51 | ||
52 | def __call__(self, request, *args, **kw): | |
53 | raise NotImplemented() | |
54 | ||
55 | ||
88a9662b | 56 | def json_response(serializable, _disable_cors=False, *args, **kw): |
85726f73 JW |
57 | ''' |
58 | Serializes a json objects and returns a webob.Response object with the | |
59 | serialized value as the response body and Content-Type: application/json. | |
60 | ||
61 | :param serializable: A json-serializable object | |
62 | ||
63 | Any extra arguments and keyword arguments are passed to the | |
64 | webob.Response.__init__ method. | |
65 | ''' | |
66 | response = Response(json.dumps(serializable), *args, **kw) | |
42c83752 | 67 | response.headers['Content-Type'] = 'application/json' |
88a9662b JW |
68 | |
69 | if not _disable_cors: | |
70 | cors_headers = { | |
71 | 'Access-Control-Allow-Origin': '*', | |
72 | 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', | |
73 | 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} | |
74 | response.headers.update(cors_headers) | |
75 | ||
42c83752 JW |
76 | return response |
77 | ||
78 | ||
85726f73 JW |
79 | def get_entry_serializable(entry, urlgen): |
80 | ''' | |
81 | Returns a serializable dict() of a MediaEntry instance. | |
82 | ||
83 | :param entry: A MediaEntry instance | |
84 | :param urlgen: An urlgen instance, can be found on the request object passed | |
85 | to views. | |
86 | ''' | |
42c83752 JW |
87 | return { |
88 | 'user': entry.get_uploader.username, | |
89 | 'user_id': entry.get_uploader.id, | |
85726f73 JW |
90 | 'user_bio': entry.get_uploader.bio, |
91 | 'user_bio_html': entry.get_uploader.bio_html, | |
92 | 'user_permalink': urlgen('mediagoblin.user_pages.user_home', | |
93 | user=entry.get_uploader.username, | |
94 | qualified=True), | |
42c83752 JW |
95 | 'id': entry.id, |
96 | 'created': entry.created.isoformat(), | |
97 | 'title': entry.title, | |
98 | 'license': entry.license, | |
99 | 'description': entry.description, | |
100 | 'description_html': entry.description_html, | |
101 | 'media_type': entry.media_type, | |
09e528ac | 102 | 'state': entry.state, |
85726f73 JW |
103 | 'permalink': entry.url_for_self(urlgen, qualified=True), |
104 | 'media_files': get_media_file_paths(entry.media_files, urlgen)} | |
105 | ||
42c83752 | 106 | |
85726f73 JW |
107 | def get_media_file_paths(media_files, urlgen): |
108 | ''' | |
109 | Returns a dictionary of media files with `file_handle` => `qualified URL` | |
42c83752 | 110 | |
85726f73 JW |
111 | :param media_files: dict-like object consisting of `file_handle => `listy |
112 | filepath` pairs. | |
113 | :param urlgen: An urlgen object, usually found on request.urlgen. | |
114 | ''' | |
42c83752 JW |
115 | media_urls = {} |
116 | ||
117 | for key, val in media_files.items(): | |
c92aa0d0 JW |
118 | if isinstance(mg_globals.public_store, BasicFileStorage): |
119 | # BasicFileStorage does not provide a qualified URI | |
120 | media_urls[key] = urljoin( | |
121 | urlgen('index', qualified=True), | |
122 | mg_globals.public_store.file_url(val)) | |
123 | else: | |
124 | media_urls[key] = mg_globals.public_store.file_url(val) | |
42c83752 JW |
125 | |
126 | return media_urls | |
127 | ||
128 | ||
a062149e | 129 | def api_auth(controller): |
85726f73 JW |
130 | ''' |
131 | Decorator, allows plugins to register auth methods that will then be | |
132 | evaluated against the request, finally a worthy authenticator object is | |
133 | chosen and used to decide whether to grant or deny access. | |
134 | ''' | |
a062149e JW |
135 | @wraps(controller) |
136 | def wrapper(request, *args, **kw): | |
137 | auth_candidates = [] | |
138 | ||
139 | for auth in PluginManager().get_hook_callables('auth'): | |
140 | _log.debug('Plugin auth: {0}'.format(auth)) | |
141 | if auth.trigger(request): | |
142 | auth_candidates.append(auth) | |
143 | ||
144 | # If we can't find any authentication methods, we should not let them | |
145 | # pass. | |
146 | if not auth_candidates: | |
62d14bf5 | 147 | return Forbidden() |
a062149e JW |
148 | |
149 | # For now, just select the first one in the list | |
150 | auth = auth_candidates[0] | |
151 | ||
152 | _log.debug('Using {0} to authorize request {1}'.format( | |
153 | auth, request.url)) | |
154 | ||
155 | if not auth(request, *args, **kw): | |
88a9662b JW |
156 | if getattr(auth, 'errors', []): |
157 | return json_response({ | |
158 | 'status': 403, | |
159 | 'errors': auth.errors}) | |
160 | ||
62d14bf5 | 161 | return Forbidden() |
a062149e JW |
162 | |
163 | return controller(request, *args, **kw) | |
164 | ||
165 | return wrapper |