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 | |
c92aa0d0 | 21 | from urlparse import urljoin |
62d14bf5 | 22 | from werkzeug.exceptions import Forbidden |
cc195d5b | 23 | from werkzeug.wrappers import Response |
42c83752 | 24 | from mediagoblin import mg_globals |
a062149e | 25 | from mediagoblin.tools.pluginapi import PluginManager |
42c83752 | 26 | from mediagoblin.storage.filestorage import BasicFileStorage |
a062149e JW |
27 | |
28 | _log = logging.getLogger(__name__) | |
29 | ||
30 | ||
42c83752 JW |
31 | class Auth(object): |
32 | ''' | |
33 | An object with two significant methods, 'trigger' and 'run'. | |
34 | ||
35 | Using a similar object to this, plugins can register specific | |
36 | authentication logic, for example the GET param 'access_token' for OAuth. | |
37 | ||
38 | - trigger: Analyze the 'request' argument, return True if you think you | |
39 | can handle the request, otherwise return False | |
40 | - run: The authentication logic, set the request.user object to the user | |
41 | you intend to authenticate and return True, otherwise return False. | |
42 | ||
43 | If run() returns False, an HTTP 403 Forbidden error will be shown. | |
44 | ||
45 | You may also display custom errors, just raise them within the run() | |
46 | method. | |
47 | ''' | |
48 | def trigger(self, request): | |
49 | raise NotImplemented() | |
50 | ||
51 | def __call__(self, request, *args, **kw): | |
52 | raise NotImplemented() | |
53 | ||
54 | ||
88a9662b | 55 | def json_response(serializable, _disable_cors=False, *args, **kw): |
85726f73 | 56 | ''' |
cc195d5b | 57 | Serializes a json objects and returns a werkzeug Response object with the |
85726f73 JW |
58 | serialized value as the response body and Content-Type: application/json. |
59 | ||
60 | :param serializable: A json-serializable object | |
61 | ||
62 | Any extra arguments and keyword arguments are passed to the | |
cc195d5b | 63 | Response.__init__ method. |
85726f73 | 64 | ''' |
cc195d5b | 65 | response = Response(json.dumps(serializable), *args, content_type='application/json', **kw) |
88a9662b JW |
66 | |
67 | if not _disable_cors: | |
68 | cors_headers = { | |
69 | 'Access-Control-Allow-Origin': '*', | |
70 | 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', | |
71 | 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} | |
7c552c0b | 72 | (response.headers.set(key, value) for key, value in cors_headers) |
88a9662b | 73 | |
42c83752 JW |
74 | return response |
75 | ||
76 | ||
85726f73 JW |
77 | def get_entry_serializable(entry, urlgen): |
78 | ''' | |
79 | Returns a serializable dict() of a MediaEntry instance. | |
80 | ||
81 | :param entry: A MediaEntry instance | |
82 | :param urlgen: An urlgen instance, can be found on the request object passed | |
83 | to views. | |
84 | ''' | |
42c83752 JW |
85 | return { |
86 | 'user': entry.get_uploader.username, | |
87 | 'user_id': entry.get_uploader.id, | |
85726f73 JW |
88 | 'user_bio': entry.get_uploader.bio, |
89 | 'user_bio_html': entry.get_uploader.bio_html, | |
90 | 'user_permalink': urlgen('mediagoblin.user_pages.user_home', | |
91 | user=entry.get_uploader.username, | |
92 | qualified=True), | |
42c83752 JW |
93 | 'id': entry.id, |
94 | 'created': entry.created.isoformat(), | |
95 | 'title': entry.title, | |
96 | 'license': entry.license, | |
97 | 'description': entry.description, | |
98 | 'description_html': entry.description_html, | |
99 | 'media_type': entry.media_type, | |
09e528ac | 100 | 'state': entry.state, |
85726f73 JW |
101 | 'permalink': entry.url_for_self(urlgen, qualified=True), |
102 | 'media_files': get_media_file_paths(entry.media_files, urlgen)} | |
103 | ||
42c83752 | 104 | |
85726f73 JW |
105 | def get_media_file_paths(media_files, urlgen): |
106 | ''' | |
107 | Returns a dictionary of media files with `file_handle` => `qualified URL` | |
42c83752 | 108 | |
85726f73 JW |
109 | :param media_files: dict-like object consisting of `file_handle => `listy |
110 | filepath` pairs. | |
111 | :param urlgen: An urlgen object, usually found on request.urlgen. | |
112 | ''' | |
42c83752 JW |
113 | media_urls = {} |
114 | ||
115 | for key, val in media_files.items(): | |
c92aa0d0 JW |
116 | if isinstance(mg_globals.public_store, BasicFileStorage): |
117 | # BasicFileStorage does not provide a qualified URI | |
118 | media_urls[key] = urljoin( | |
119 | urlgen('index', qualified=True), | |
120 | mg_globals.public_store.file_url(val)) | |
121 | else: | |
122 | media_urls[key] = mg_globals.public_store.file_url(val) | |
42c83752 JW |
123 | |
124 | return media_urls | |
125 | ||
126 | ||
a062149e | 127 | def api_auth(controller): |
85726f73 JW |
128 | ''' |
129 | Decorator, allows plugins to register auth methods that will then be | |
130 | evaluated against the request, finally a worthy authenticator object is | |
131 | chosen and used to decide whether to grant or deny access. | |
132 | ''' | |
a062149e JW |
133 | @wraps(controller) |
134 | def wrapper(request, *args, **kw): | |
135 | auth_candidates = [] | |
136 | ||
137 | for auth in PluginManager().get_hook_callables('auth'): | |
a062149e | 138 | if auth.trigger(request): |
57c6473a | 139 | _log.debug('{0} believes it is capable of authenticating this request.'.format(auth)) |
a062149e JW |
140 | auth_candidates.append(auth) |
141 | ||
142 | # If we can't find any authentication methods, we should not let them | |
143 | # pass. | |
144 | if not auth_candidates: | |
cfa92229 | 145 | raise Forbidden() |
a062149e JW |
146 | |
147 | # For now, just select the first one in the list | |
148 | auth = auth_candidates[0] | |
149 | ||
150 | _log.debug('Using {0} to authorize request {1}'.format( | |
151 | auth, request.url)) | |
152 | ||
153 | if not auth(request, *args, **kw): | |
88a9662b JW |
154 | if getattr(auth, 'errors', []): |
155 | return json_response({ | |
156 | 'status': 403, | |
157 | 'errors': auth.errors}) | |
158 | ||
cfa92229 | 159 | raise Forbidden() |
a062149e JW |
160 | |
161 | return controller(request, *args, **kw) | |
162 | ||
163 | return wrapper |