Finishes most of oauth, just decorator to complete
[mediagoblin.git] / mediagoblin / federation / views.py
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 datetime
18
19 import oauthlib.common
20 from oauthlib.oauth1 import (AuthorizationEndpoint, RequestValidator,
21 RequestTokenEndpoint, AccessTokenEndpoint)
22
23 from mediagoblin.decorators import require_active_login
24 from mediagoblin.tools.translate import pass_to_ugettext
25 from mediagoblin.meddleware.csrf import csrf_exempt
26 from mediagoblin.tools.request import decode_request, decode_authorization_header
27 from mediagoblin.tools.response import (render_to_response, redirect,
28 json_response, render_400,
29 form_response)
30 from mediagoblin.tools.crypto import random_string
31 from mediagoblin.tools.validator import validate_email, validate_url
32 from mediagoblin.db.models import User, Client, RequestToken, AccessToken
33 from mediagoblin.federation.forms import AuthorizeForm
34
35 # possible client types
36 client_types = ["web", "native"] # currently what pump supports
37
38 @csrf_exempt
39 def client_register(request):
40 """ Endpoint for client registration """
41 try:
42 data = decode_request(request)
43 except ValueError:
44 error = "Could not decode data."
45 return json_response({"error": error}, status=400)
46
47 if data is "":
48 error = "Unknown Content-Type"
49 return json_response({"error": error}, status=400)
50
51 if "type" not in data:
52 error = "No registration type provided."
53 return json_response({"error": error}, status=400)
54 if data.get("application_type", None) not in client_types:
55 error = "Unknown application_type."
56 return json_response({"error": error}, status=400)
57
58 client_type = data["type"]
59
60 if client_type == "client_update":
61 # updating a client
62 if "client_id" not in data:
63 error = "client_id is requried to update."
64 return json_response({"error": error}, status=400)
65 elif "client_secret" not in data:
66 error = "client_secret is required to update."
67 return json_response({"error": error}, status=400)
68
69 client = Client.query.filter_by(
70 id=data["client_id"],
71 secret=data["client_secret"]
72 ).first()
73
74 if client is None:
75 error = "Unauthorized."
76 return json_response({"error": error}, status=403)
77
78 client.application_name = data.get(
79 "application_name",
80 client.application_name
81 )
82
83 client.application_type = data.get(
84 "application_type",
85 client.application_type
86 )
87
88 app_name = ("application_type", client.application_name)
89 if app_name in client_types:
90 client.application_name = app_name
91
92 elif client_type == "client_associate":
93 # registering
94 if "client_id" in data:
95 error = "Only set client_id for update."
96 return json_response({"error": error}, status=400)
97 elif "access_token" in data:
98 error = "access_token not needed for registration."
99 return json_response({"error": error}, status=400)
100 elif "client_secret" in data:
101 error = "Only set client_secret for update."
102 return json_response({"error": error}, status=400)
103
104 # generate the client_id and client_secret
105 client_id = random_string(22) # seems to be what pump uses
106 client_secret = random_string(43) # again, seems to be what pump uses
107 expirey = 0 # for now, lets not have it expire
108 expirey_db = None if expirey == 0 else expirey
109
110 # save it
111 client = Client(
112 id=client_id,
113 secret=client_secret,
114 expirey=expirey_db,
115 application_type=data["application_type"],
116 )
117
118 else:
119 error = "Invalid registration type"
120 return json_response({"error": error}, status=400)
121
122 logo_url = data.get("logo_url", client.logo_url)
123 if logo_url is not None and not validate_url(logo_url):
124 error = "Logo URL {0} is not a valid URL.".format(logo_url)
125 return json_response(
126 {"error": error},
127 status=400
128 )
129 else:
130 client.logo_url = logo_url
131
132 client.application_name = data.get("application_name", None)
133
134 contacts = data.get("contact", None)
135 if contacts is not None:
136 if type(contacts) is not unicode:
137 error = "Contacts must be a string of space-seporated email addresses."
138 return json_response({"error": error}, status=400)
139
140 contacts = contacts.split()
141 for contact in contacts:
142 if not validate_email(contact):
143 # not a valid email
144 error = "Email {0} is not a valid email.".format(contact)
145 return json_response({"error": error}, status=400)
146
147
148 client.contacts = contacts
149
150 request_uri = data.get("request_uris", None)
151 if request_uri is not None:
152 if type(request_uri) is not unicode:
153 error = "redirect_uris must be space-seporated URLs."
154 return json_respinse({"error": error}, status=400)
155
156 request_uri = request_uri.split()
157
158 for uri in request_uri:
159 if not validate_url(uri):
160 # not a valid uri
161 error = "URI {0} is not a valid URI".format(uri)
162 return json_response({"error": error}, status=400)
163
164 client.request_uri = request_uri
165
166
167 client.save()
168
169 expirey = 0 if client.expirey is None else client.expirey
170
171 return json_response(
172 {
173 "client_id": client.id,
174 "client_secret": client.secret,
175 "expires_at": expirey,
176 })
177
178 class ValidationException(Exception):
179 pass
180
181 class GMGRequestValidator(RequestValidator):
182
183 def __init__(self, data=None):
184 self.POST = data
185
186 def save_request_token(self, token, request):
187 """ Saves request token in db """
188 client_id = self.POST[u"oauth_consumer_key"]
189
190 request_token = RequestToken(
191 token=token["oauth_token"],
192 secret=token["oauth_token_secret"],
193 )
194 request_token.client = client_id
195 request_token.callback = token.get("oauth_callback", None)
196 request_token.save()
197
198 def save_verifier(self, token, verifier, request):
199 """ Saves the oauth request verifier """
200 request_token = RequestToken.query.filter_by(token=token).first()
201 request_token.verifier = verifier["oauth_verifier"]
202 request_token.save()
203
204 def save_access_token(self, token, request):
205 """ Saves access token in db """
206 access_token = AccessToken(
207 token=token["oauth_token"],
208 secret=token["oauth_token_secret"],
209 )
210 access_token.request_token = request.oauth_token
211 request_token = RequestToken.query.filter_by(token=request.oauth_token).first()
212 access_token.user = request_token.user
213 access_token.save()
214
215 def get_realms(*args, **kwargs):
216 """ Currently a stub - called when making AccessTokens """
217 return list()
218
219 @csrf_exempt
220 def request_token(request):
221 """ Returns request token """
222 try:
223 data = decode_request(request)
224 except ValueError:
225 error = "Could not decode data."
226 return json_response({"error": error}, status=400)
227
228 if data == "":
229 error = "Unknown Content-Type"
230 return json_response({"error": error}, status=400)
231
232 if not data and request.headers:
233 data = request.headers
234
235 data = dict(data) # mutableifying
236
237 authorization = decode_authorization_header(data)
238
239
240 if authorization == dict() or u"oauth_consumer_key" not in authorization:
241 error = "Missing required parameter."
242 return json_response({"error": error}, status=400)
243
244 # check the client_id
245 client_id = authorization[u"oauth_consumer_key"]
246 client = Client.query.filter_by(id=client_id).first()
247 if client is None:
248 # client_id is invalid
249 error = "Invalid client_id"
250 return json_response({"error": error}, status=400)
251
252 # make request token and return to client
253 request_validator = GMGRequestValidator(authorization)
254 rv = RequestTokenEndpoint(request_validator)
255 tokens = rv.create_request_token(request, authorization)
256
257 return form_response(tokens)
258
259 class WTFormData(dict):
260 """
261 Provides a WTForm usable dictionary
262 """
263 def getlist(self, key):
264 v = self[key]
265 if not isinstance(v, (list, tuple)):
266 v = [v]
267 return v
268
269 @require_active_login
270 def authorize(request):
271 """ Displays a page for user to authorize """
272 if request.method == "POST":
273 return authorize_finish(request)
274
275 _ = pass_to_ugettext
276 token = request.args.get("oauth_token", None)
277 if token is None:
278 # no token supplied, display a html 400 this time
279 err_msg = _("Must provide an oauth_token.")
280 return render_400(request, err_msg=err_msg)
281
282 oauth_request = RequestToken.query.filter_by(token=token).first()
283 if oauth_request is None:
284 err_msg = _("No request token found.")
285 return render_400(request, err_msg)
286
287 if oauth_request.used:
288 return authorize_finish(request)
289
290 if oauth_request.verifier is None:
291 orequest = oauthlib.common.Request(
292 uri=request.url,
293 http_method=request.method,
294 body=request.get_data(),
295 headers=request.headers
296 )
297 request_validator = GMGRequestValidator()
298 auth_endpoint = AuthorizationEndpoint(request_validator)
299 verifier = auth_endpoint.create_verifier(orequest, {})
300 oauth_request.verifier = verifier["oauth_verifier"]
301
302 oauth_request.user = request.user.id
303 oauth_request.save()
304
305 # find client & build context
306 client = Client.query.filter_by(id=oauth_request.client).first()
307
308 authorize_form = AuthorizeForm(WTFormData({
309 "oauth_token": oauth_request.token,
310 "oauth_verifier": oauth_request.verifier
311 }))
312
313 context = {
314 "user": request.user,
315 "oauth_request": oauth_request,
316 "client": client,
317 "authorize_form": authorize_form,
318 }
319
320
321 # AuthorizationEndpoint
322 return render_to_response(
323 request,
324 "mediagoblin/api/authorize.html",
325 context
326 )
327
328
329 def authorize_finish(request):
330 """ Finishes the authorize """
331 _ = pass_to_ugettext
332 token = request.form["oauth_token"]
333 verifier = request.form["oauth_verifier"]
334 oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier)
335 oauth_request = oauth_request.first()
336
337 if oauth_request is None:
338 # invalid token or verifier
339 err_msg = _("No request token found.")
340 return render_400(request, err_msg)
341
342 oauth_request.used = True
343 oauth_request.updated = datetime.datetime.now()
344 oauth_request.save()
345
346 if oauth_request.callback == "oob":
347 # out of bounds
348 context = {"oauth_request": oauth_request}
349 return render_to_response(
350 request,
351 "mediagoblin/api/oob.html",
352 context
353 )
354
355 # okay we need to redirect them then!
356 querystring = "?oauth_token={0}&oauth_verifier={1}".format(
357 oauth_request.token,
358 oauth_request.verifier
359 )
360
361 return redirect(
362 request,
363 querystring=querystring,
364 location=oauth_request.callback
365 )
366
367 @csrf_exempt
368 def access_token(request):
369 """ Provides an access token based on a valid verifier and request token """
370 data = request.headers
371
372 parsed_tokens = decode_authorization_header(data)
373
374 if parsed_tokens == dict() or "oauth_token" not in parsed_tokens:
375 error = "Missing required parameter."
376 return json_response({"error": error}, status=400)
377
378
379 request.oauth_token = parsed_tokens["oauth_token"]
380 request_validator = GMGRequestValidator(data)
381 av = AccessTokenEndpoint(request_validator)
382 tokens = av.create_access_token(request, {})
383 return form_response(tokens)
384