Clean up & Add support to update objects in feed API
[mediagoblin.git] / mediagoblin / oauth / views.py
CommitLineData
c840cb66 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
405aa45a 17import datetime
c5eb24b8 18import string
4990b47c 19
617bff18
JT
20from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint,
21 AccessTokenEndpoint)
42dbb26a 22
617bff18 23from mediagoblin.decorators import require_active_login
d41c6a53 24from mediagoblin.tools.translate import pass_to_ugettext
4990b47c 25from mediagoblin.meddleware.csrf import csrf_exempt
786bbd79 26from mediagoblin.tools.request import decode_request
42dbb26a 27from mediagoblin.tools.response import (render_to_response, redirect,
2b60a56c 28 json_response, render_400,
29 form_response)
4990b47c 30from mediagoblin.tools.crypto import random_string
c33a34d4 31from mediagoblin.tools.validator import validate_email, validate_url
005181b1 32from mediagoblin.oauth.forms import AuthorizeForm
33from mediagoblin.oauth.oauth import GMGRequestValidator, GMGRequest
34from mediagoblin.oauth.tools.request import decode_authorization_header
35from mediagoblin.oauth.tools.forms import WTFormData
617bff18 36from mediagoblin.db.models import NonceTimestamp, Client, RequestToken
c840cb66 37
38# possible client types
c5eb24b8
JT
39CLIENT_TYPES = ["web", "native"] # currently what pump supports
40OAUTH_ALPHABET = (string.ascii_letters.decode('ascii') +
41 string.digits.decode('ascii'))
c840cb66 42
4990b47c 43@csrf_exempt
c840cb66 44def client_register(request):
45 """ Endpoint for client registration """
d41c6a53 46 try:
42dbb26a 47 data = decode_request(request)
d41c6a53 48 except ValueError:
49 error = "Could not decode data."
50 return json_response({"error": error}, status=400)
51
52 if data is "":
53 error = "Unknown Content-Type"
54 return json_response({"error": error}, status=400)
c840cb66 55
4990b47c 56 if "type" not in data:
d41c6a53 57 error = "No registration type provided."
58 return json_response({"error": error}, status=400)
c5eb24b8 59 if data.get("application_type", None) not in CLIENT_TYPES:
d41c6a53 60 error = "Unknown application_type."
61 return json_response({"error": error}, status=400)
42dbb26a 62
54fbbf09 63 client_type = data["type"]
64
65 if client_type == "client_update":
66 # updating a client
67 if "client_id" not in data:
d41c6a53 68 error = "client_id is requried to update."
69 return json_response({"error": error}, status=400)
54fbbf09 70 elif "client_secret" not in data:
d41c6a53 71 error = "client_secret is required to update."
72 return json_response({"error": error}, status=400)
54fbbf09 73
d41c6a53 74 client = Client.query.filter_by(
42dbb26a 75 id=data["client_id"],
d41c6a53 76 secret=data["client_secret"]
77 ).first()
54fbbf09 78
c33a34d4 79 if client is None:
d41c6a53 80 error = "Unauthorized."
81 return json_response({"error": error}, status=403)
82
83 client.application_name = data.get(
42dbb26a 84 "application_name",
d41c6a53 85 client.application_name
86 )
87
88 client.application_type = data.get(
89 "application_type",
90 client.application_type
91 )
54fbbf09 92
763e300d 93 app_name = ("application_type", client.application_name)
c5eb24b8 94 if app_name in CLIENT_TYPES:
763e300d 95 client.application_name = app_name
763e300d 96
54fbbf09 97 elif client_type == "client_associate":
98 # registering
99 if "client_id" in data:
d41c6a53 100 error = "Only set client_id for update."
101 return json_response({"error": error}, status=400)
54fbbf09 102 elif "access_token" in data:
d41c6a53 103 error = "access_token not needed for registration."
104 return json_response({"error": error}, status=400)
54fbbf09 105 elif "client_secret" in data:
d41c6a53 106 error = "Only set client_secret for update."
107 return json_response({"error": error}, status=400)
54fbbf09 108
c33a34d4 109 # generate the client_id and client_secret
c5eb24b8
JT
110 client_id = random_string(22, OAUTH_ALPHABET)
111 client_secret = random_string(43, OAUTH_ALPHABET)
c33a34d4 112 expirey = 0 # for now, lets not have it expire
113 expirey_db = None if expirey == 0 else expirey
42dbb26a
RE
114 application_type = data["application_type"]
115
c33a34d4 116 # save it
117 client = Client(
42dbb26a
RE
118 id=client_id,
119 secret=client_secret,
c33a34d4 120 expirey=expirey_db,
86ba4168 121 application_type=application_type,
d41c6a53 122 )
c33a34d4 123
124 else:
d41c6a53 125 error = "Invalid registration type"
126 return json_response({"error": error}, status=400)
c33a34d4 127
128 logo_url = data.get("logo_url", client.logo_url)
129 if logo_url is not None and not validate_url(logo_url):
d41c6a53 130 error = "Logo URL {0} is not a valid URL.".format(logo_url)
131 return json_response(
42dbb26a 132 {"error": error},
d41c6a53 133 status=400
134 )
c33a34d4 135 else:
136 client.logo_url = logo_url
42dbb26a 137
405aa45a 138 client.application_name = data.get("application_name", None)
c33a34d4 139
86ba4168 140 contacts = data.get("contacts", None)
c33a34d4 141 if contacts is not None:
142 if type(contacts) is not unicode:
d41c6a53 143 error = "Contacts must be a string of space-seporated email addresses."
144 return json_response({"error": error}, status=400)
c33a34d4 145
146 contacts = contacts.split()
147 for contact in contacts:
148 if not validate_email(contact):
149 # not a valid email
d41c6a53 150 error = "Email {0} is not a valid email.".format(contact)
151 return json_response({"error": error}, status=400)
42dbb26a
RE
152
153
c33a34d4 154 client.contacts = contacts
155
86ba4168 156 redirect_uris = data.get("redirect_uris", None)
157 if redirect_uris is not None:
158 if type(redirect_uris) is not unicode:
d41c6a53 159 error = "redirect_uris must be space-seporated URLs."
617bff18 160 return json_response({"error": error}, status=400)
c33a34d4 161
86ba4168 162 redirect_uris = redirect_uris.split()
c33a34d4 163
86ba4168 164 for uri in redirect_uris:
c33a34d4 165 if not validate_url(uri):
166 # not a valid uri
d41c6a53 167 error = "URI {0} is not a valid URI".format(uri)
168 return json_response({"error": error}, status=400)
c33a34d4 169
86ba4168 170 client.redirect_uri = redirect_uris
c33a34d4 171
42dbb26a 172
4990b47c 173 client.save()
c840cb66 174
c33a34d4 175 expirey = 0 if client.expirey is None else client.expirey
176
4990b47c 177 return json_response(
178 {
d41c6a53 179 "client_id": client.id,
180 "client_secret": client.secret,
181 "expires_at": expirey,
4990b47c 182 })
d41c6a53 183
d41c6a53 184@csrf_exempt
185def request_token(request):
186 """ Returns request token """
187 try:
42dbb26a 188 data = decode_request(request)
d41c6a53 189 except ValueError:
190 error = "Could not decode data."
191 return json_response({"error": error}, status=400)
192
405aa45a 193 if data == "":
d41c6a53 194 error = "Unknown Content-Type"
195 return json_response({"error": error}, status=400)
196
2b60a56c 197 if not data and request.headers:
198 data = request.headers
42dbb26a 199
2b60a56c 200 data = dict(data) # mutableifying
405aa45a 201
2b60a56c 202 authorization = decode_authorization_header(data)
d41c6a53 203
2b60a56c 204 if authorization == dict() or u"oauth_consumer_key" not in authorization:
405aa45a 205 error = "Missing required parameter."
2b60a56c 206 return json_response({"error": error}, status=400)
405aa45a 207
d41c6a53 208 # check the client_id
2b60a56c 209 client_id = authorization[u"oauth_consumer_key"]
d41c6a53 210 client = Client.query.filter_by(id=client_id).first()
89d5b44e 211
212 if client == None:
d41c6a53 213 # client_id is invalid
214 error = "Invalid client_id"
215 return json_response({"error": error}, status=400)
216
89d5b44e 217 # make request token and return to client
2b60a56c 218 request_validator = GMGRequestValidator(authorization)
d41c6a53 219 rv = RequestTokenEndpoint(request_validator)
405aa45a 220 tokens = rv.create_request_token(request, authorization)
d41c6a53 221
cfe7054c 222 # store the nonce & timestamp before we return back
223 nonce = authorization[u"oauth_nonce"]
224 timestamp = authorization[u"oauth_timestamp"]
89d5b44e 225 timestamp = datetime.datetime.fromtimestamp(float(timestamp))
cfe7054c 226
227 nc = NonceTimestamp(nonce=nonce, timestamp=timestamp)
228 nc.save()
229
2b60a56c 230 return form_response(tokens)
405aa45a 231
42dbb26a 232@require_active_login
d41c6a53 233def authorize(request):
234 """ Displays a page for user to authorize """
405aa45a 235 if request.method == "POST":
236 return authorize_finish(request)
42dbb26a 237
d41c6a53 238 _ = pass_to_ugettext
239 token = request.args.get("oauth_token", None)
240 if token is None:
241 # no token supplied, display a html 400 this time
405aa45a 242 err_msg = _("Must provide an oauth_token.")
d41c6a53 243 return render_400(request, err_msg=err_msg)
244
405aa45a 245 oauth_request = RequestToken.query.filter_by(token=token).first()
246 if oauth_request is None:
247 err_msg = _("No request token found.")
248 return render_400(request, err_msg)
42dbb26a 249
405aa45a 250 if oauth_request.used:
251 return authorize_finish(request)
42dbb26a 252
405aa45a 253 if oauth_request.verifier is None:
786bbd79 254 orequest = GMGRequest(request)
405aa45a 255 request_validator = GMGRequestValidator()
256 auth_endpoint = AuthorizationEndpoint(request_validator)
257 verifier = auth_endpoint.create_verifier(orequest, {})
258 oauth_request.verifier = verifier["oauth_verifier"]
259
260 oauth_request.user = request.user.id
261 oauth_request.save()
262
263 # find client & build context
264 client = Client.query.filter_by(id=oauth_request.client).first()
265
266 authorize_form = AuthorizeForm(WTFormData({
267 "oauth_token": oauth_request.token,
268 "oauth_verifier": oauth_request.verifier
269 }))
270
271 context = {
272 "user": request.user,
273 "oauth_request": oauth_request,
274 "client": client,
275 "authorize_form": authorize_form,
276 }
277
278
d41c6a53 279 # AuthorizationEndpoint
405aa45a 280 return render_to_response(
281 request,
282 "mediagoblin/api/authorize.html",
283 context
284 )
42dbb26a 285
405aa45a 286
287def authorize_finish(request):
288 """ Finishes the authorize """
289 _ = pass_to_ugettext
290 token = request.form["oauth_token"]
291 verifier = request.form["oauth_verifier"]
292 oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier)
293 oauth_request = oauth_request.first()
42dbb26a 294
405aa45a 295 if oauth_request is None:
296 # invalid token or verifier
297 err_msg = _("No request token found.")
298 return render_400(request, err_msg)
299
300 oauth_request.used = True
301 oauth_request.updated = datetime.datetime.now()
302 oauth_request.save()
303
304 if oauth_request.callback == "oob":
305 # out of bounds
306 context = {"oauth_request": oauth_request}
307 return render_to_response(
308 request,
309 "mediagoblin/api/oob.html",
310 context
311 )
312
313 # okay we need to redirect them then!
314 querystring = "?oauth_token={0}&oauth_verifier={1}".format(
315 oauth_request.token,
316 oauth_request.verifier
317 )
318
319 return redirect(
320 request,
321 querystring=querystring,
322 location=oauth_request.callback
323 )
d41c6a53 324
325@csrf_exempt
326def access_token(request):
42dbb26a 327 """ Provides an access token based on a valid verifier and request token """
2b60a56c 328 data = request.headers
405aa45a 329
42dbb26a 330 parsed_tokens = decode_authorization_header(data)
2b60a56c 331
332 if parsed_tokens == dict() or "oauth_token" not in parsed_tokens:
333 error = "Missing required parameter."
405aa45a 334 return json_response({"error": error}, status=400)
335
2b60a56c 336
337 request.oauth_token = parsed_tokens["oauth_token"]
338 request_validator = GMGRequestValidator(data)
339 av = AccessTokenEndpoint(request_validator)
340 tokens = av.create_access_token(request, {})
341 return form_response(tokens)
1e2675b0 342