Added client registration caps to OAuth plugin
THE MIGRATIONS SUPPLIED WITH THIS COMMIT WILL DROP AND RE-CREATE YOUR oauth__tokens AND oauth__codes TABLES. ALL YOUR OAUTH CODES AND TOKENS WILL BE LOST. - Fixed pylint issues in db/sql/migrations. - Added __repr__ to the User model. - Added _disable_cors option to json_response. - Added crude error handling to the api.tools.api_auth decorator - Updated the OAuth README. - Added client registration, client overview, connection overview, client authorization views and templates. - Added error handling to the OAuthAuth Auth object. - Added AuthorizationForm, ClientRegistrationForm in oauth/forms. - Added migrations for OAuth, added client registration migration. - Added OAuthClient, OAuthUserClient models. - Added oauth/tools with require_client_auth decorator method.
This commit is contained in:
parent
d4c066abf0
commit
88a9662be4
@ -19,12 +19,9 @@ import datetime
|
||||
from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger,
|
||||
Integer, Unicode, UnicodeText, DateTime, ForeignKey)
|
||||
|
||||
|
||||
|
||||
from mediagoblin.db.sql.util import RegisterMigration
|
||||
from mediagoblin.db.sql.models import MediaEntry, Collection, User
|
||||
|
||||
|
||||
MIGRATIONS = {}
|
||||
|
||||
|
||||
@ -66,6 +63,7 @@ def add_transcoding_progress(db_conn):
|
||||
col.create(media_entry)
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
@RegisterMigration(4, MIGRATIONS)
|
||||
def add_collection_tables(db_conn):
|
||||
metadata = MetaData(bind=db_conn.bind)
|
||||
@ -92,6 +90,7 @@ def add_collection_tables(db_conn):
|
||||
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
@RegisterMigration(5, MIGRATIONS)
|
||||
def add_mediaentry_collected(db_conn):
|
||||
metadata = MetaData(bind=db_conn.bind)
|
||||
@ -102,4 +101,3 @@ def add_mediaentry_collected(db_conn):
|
||||
col = Column('collected', Integer, default=0)
|
||||
col.create(media_entry)
|
||||
db_conn.commit()
|
||||
|
||||
|
@ -85,6 +85,14 @@ class User(Base, UserMixin):
|
||||
|
||||
_id = SimpleFieldAlias("id")
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0} #{1} {2} {3} "{4}">'.format(
|
||||
self.__class__.__name__,
|
||||
self.id,
|
||||
'verified' if self.email_verified else 'non-verified',
|
||||
'admin' if self.is_admin else 'user',
|
||||
self.username)
|
||||
|
||||
|
||||
class MediaEntry(Base, MediaEntryMixin):
|
||||
"""
|
||||
|
@ -52,7 +52,7 @@ class Auth(object):
|
||||
raise NotImplemented()
|
||||
|
||||
|
||||
def json_response(serializable, *args, **kw):
|
||||
def json_response(serializable, _disable_cors=False, *args, **kw):
|
||||
'''
|
||||
Serializes a json objects and returns a webob.Response object with the
|
||||
serialized value as the response body and Content-Type: application/json.
|
||||
@ -64,11 +64,14 @@ def json_response(serializable, *args, **kw):
|
||||
'''
|
||||
response = Response(json.dumps(serializable), *args, **kw)
|
||||
response.headers['Content-Type'] = 'application/json'
|
||||
|
||||
if not _disable_cors:
|
||||
cors_headers = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'}
|
||||
response.headers.update(cors_headers)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@ -149,6 +152,11 @@ def api_auth(controller):
|
||||
auth, request.url))
|
||||
|
||||
if not auth(request, *args, **kw):
|
||||
if getattr(auth, 'errors', []):
|
||||
return json_response({
|
||||
'status': 403,
|
||||
'errors': auth.errors})
|
||||
|
||||
return exc.HTTPForbidden()
|
||||
|
||||
return controller(request, *args, **kw)
|
||||
|
@ -122,20 +122,21 @@ Capabilities
|
||||
- `Authorization endpoint`_ - Located at ``/oauth/authorize``
|
||||
- `Token endpoint`_ - Located at ``/oauth/access_token``
|
||||
- `Authorization Code Grant`_
|
||||
- `Client Registration`_
|
||||
|
||||
.. _`Authorization endpoint`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.1
|
||||
.. _`Token endpoint`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.2
|
||||
.. _`Authorization Code Grant`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-4.1
|
||||
.. _`Client Registration`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-2
|
||||
|
||||
Incapabilities
|
||||
==============
|
||||
|
||||
- `Client Registration`_ - `planned feature
|
||||
<http://issues.mediagoblin.org/ticket/497>`_
|
||||
- Only `bearer tokens`_ are issued.
|
||||
- `Access Token Scope`_
|
||||
- `Implicit Grant`_
|
||||
- ...
|
||||
|
||||
.. _`Client Registration`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-2
|
||||
.. _`bearer tokens`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08
|
||||
.. _`Access Token Scope`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.3
|
||||
.. _`Implicit Grant`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-4.2
|
||||
|
@ -18,11 +18,10 @@ import os
|
||||
import logging
|
||||
|
||||
from routes.route import Route
|
||||
from webob import exc
|
||||
|
||||
from mediagoblin.tools import pluginapi
|
||||
from mediagoblin.tools.response import render_to_response
|
||||
from mediagoblin.plugins.oauth.models import OAuthToken
|
||||
from mediagoblin.plugins.oauth.models import OAuthToken, OAuthClient, \
|
||||
OAuthUserClient
|
||||
from mediagoblin.plugins.api.tools import Auth
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
@ -39,8 +38,19 @@ def setup_plugin():
|
||||
routes = [
|
||||
Route('mediagoblin.plugins.oauth.authorize', '/oauth/authorize',
|
||||
controller='mediagoblin.plugins.oauth.views:authorize'),
|
||||
Route('mediagoblin.plugins.oauth.authorize_client', '/oauth/client/authorize',
|
||||
controller='mediagoblin.plugins.oauth.views:authorize_client'),
|
||||
Route('mediagoblin.plugins.oauth.access_token', '/oauth/access_token',
|
||||
controller='mediagoblin.plugins.oauth.views:access_token')]
|
||||
controller='mediagoblin.plugins.oauth.views:access_token'),
|
||||
Route('mediagoblin.plugins.oauth.access_token',
|
||||
'/oauth/client/connections',
|
||||
controller='mediagoblin.plugins.oauth.views:list_connections'),
|
||||
Route('mediagoblin.plugins.oauth.register_client',
|
||||
'/oauth/client/register',
|
||||
controller='mediagoblin.plugins.oauth.views:register_client'),
|
||||
Route('mediagoblin.plugins.oauth.list_clients',
|
||||
'/oauth/client/list',
|
||||
controller='mediagoblin.plugins.oauth.views:list_clients')]
|
||||
|
||||
pluginapi.register_routes(routes)
|
||||
pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
|
||||
@ -54,17 +64,42 @@ class OAuthAuth(Auth):
|
||||
return False
|
||||
|
||||
def __call__(self, request, *args, **kw):
|
||||
self.errors = []
|
||||
# TODO: Add suport for client credentials authorization
|
||||
client_id = request.GET.get('client_id') # TODO: Not used
|
||||
client_secret = request.GET.get('client_secret') # TODO: Not used
|
||||
access_token = request.GET.get('access_token')
|
||||
|
||||
_log.debug('Authorizing request {0}'.format(request.url))
|
||||
|
||||
if access_token:
|
||||
token = OAuthToken.query.filter(OAuthToken.token == access_token)\
|
||||
.first()
|
||||
|
||||
if not token:
|
||||
self.errors.append('Invalid access token')
|
||||
return False
|
||||
|
||||
_log.debug('Access token: {0}'.format(token))
|
||||
_log.debug('Client: {0}'.format(token.client))
|
||||
|
||||
relation = OAuthUserClient.query.filter(
|
||||
(OAuthUserClient.user == token.user)
|
||||
& (OAuthUserClient.client == token.client)
|
||||
& (OAuthUserClient.state == u'approved')).first()
|
||||
|
||||
_log.debug('Relation: {0}'.format(relation))
|
||||
|
||||
if not relation:
|
||||
self.errors.append(
|
||||
u'Client has not been approved by the resource owner')
|
||||
return False
|
||||
|
||||
request.user = token.user
|
||||
return True
|
||||
|
||||
self.errors.append(u'No access_token specified')
|
||||
|
||||
return False
|
||||
|
||||
hooks = {
|
||||
|
70
mediagoblin/plugins/oauth/forms.py
Normal file
70
mediagoblin/plugins/oauth/forms.py
Normal file
@ -0,0 +1,70 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import wtforms
|
||||
|
||||
from urlparse import urlparse
|
||||
|
||||
from mediagoblin.tools.extlib.wtf_html5 import URLField
|
||||
from mediagoblin.tools.translate import fake_ugettext_passthrough as _
|
||||
|
||||
|
||||
class AuthorizationForm(wtforms.Form):
|
||||
client_id = wtforms.HiddenField(_(u'Client ID'),
|
||||
[wtforms.validators.Required()])
|
||||
next = wtforms.HiddenField(_(u'Next URL'),
|
||||
[wtforms.validators.Required()])
|
||||
allow = wtforms.SubmitField(_(u'Allow'))
|
||||
deny = wtforms.SubmitField(_(u'Deny'))
|
||||
|
||||
|
||||
class ClientRegistrationForm(wtforms.Form):
|
||||
name = wtforms.TextField(_('Name'), [wtforms.validators.Required()],
|
||||
description=_('The name of the OAuth client'))
|
||||
description = wtforms.TextAreaField(_('Description'),
|
||||
[wtforms.validators.Length(min=0, max=500)],
|
||||
description=_('''This will be visisble to users allowing your
|
||||
appplication to authenticate as them.'''))
|
||||
type = wtforms.SelectField(_('Type'),
|
||||
[wtforms.validators.Required()],
|
||||
choices=[
|
||||
('confidential', 'Confidential'),
|
||||
('public', 'Public')],
|
||||
description=_('''<strong>Confidential</strong> - The client can
|
||||
make requests to the GNU MediaGoblin instance that can not be
|
||||
intercepted by the user agent (e.g. server-side client).<br />
|
||||
<strong>Public</strong> - The client can't make confidential
|
||||
requests to the GNU MediaGoblin instance (e.g. client-side
|
||||
JavaScript client).'''))
|
||||
|
||||
redirect_uri = URLField(_('Redirect URI'),
|
||||
[wtforms.validators.Optional(), wtforms.validators.URL()],
|
||||
description=_('''The redirect URI for the applications, this field
|
||||
is <strong>required</strong> for public clients.'''))
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
wtforms.Form.__init__(self, *args, **kw)
|
||||
|
||||
def validate(self):
|
||||
if not wtforms.Form.validate(self):
|
||||
return False
|
||||
|
||||
if self.type.data == 'public' and not self.redirect_uri.data:
|
||||
self.redirect_uri.errors.append(
|
||||
_('This field is required for public clients'))
|
||||
return False
|
||||
|
||||
return True
|
46
mediagoblin/plugins/oauth/migrations.py
Normal file
46
mediagoblin/plugins/oauth/migrations.py
Normal file
@ -0,0 +1,46 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from sqlalchemy import MetaData, Table
|
||||
|
||||
from mediagoblin.db.sql.util import RegisterMigration
|
||||
|
||||
from mediagoblin.plugins.oauth.models import OAuthClient, OAuthToken, \
|
||||
OAuthUserClient, OAuthCode
|
||||
|
||||
MIGRATIONS = {}
|
||||
|
||||
|
||||
@RegisterMigration(1, MIGRATIONS)
|
||||
def remove_and_replace_token_and_code(db):
|
||||
metadata = MetaData(bind=db.bind)
|
||||
|
||||
token_table = Table('oauth__tokens', metadata, autoload=True,
|
||||
autoload_with=db.bind)
|
||||
|
||||
token_table.drop()
|
||||
|
||||
code_table = Table('oauth__codes', metadata, autoload=True,
|
||||
autoload_with=db.bind)
|
||||
|
||||
code_table.drop()
|
||||
|
||||
OAuthClient.__table__.create(db.bind)
|
||||
OAuthUserClient.__table__.create(db.bind)
|
||||
OAuthToken.__table__.create(db.bind)
|
||||
OAuthCode.__table__.create(db.bind)
|
||||
|
||||
db.commit()
|
@ -14,15 +14,84 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
import bcrypt
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from mediagoblin.db.sql.base import Base
|
||||
from mediagoblin.db.sql.models import User
|
||||
|
||||
from sqlalchemy import (
|
||||
Column, Unicode, Integer, DateTime, ForeignKey)
|
||||
Column, Unicode, Integer, DateTime, ForeignKey, Enum)
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
# Don't remove this, I *think* it applies sqlalchemy-migrate functionality onto
|
||||
# the models.
|
||||
from migrate import changeset
|
||||
|
||||
|
||||
class OAuthClient(Base):
|
||||
__tablename__ = 'oauth__client'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
created = Column(DateTime, nullable=False,
|
||||
default=datetime.now)
|
||||
|
||||
name = Column(Unicode)
|
||||
description = Column(Unicode)
|
||||
|
||||
identifier = Column(Unicode, unique=True, index=True)
|
||||
secret = Column(Unicode, index=True)
|
||||
|
||||
owner_id = Column(Integer, ForeignKey(User.id))
|
||||
owner = relationship(User, backref='registered_clients')
|
||||
|
||||
redirect_uri = Column(Unicode)
|
||||
|
||||
type = Column(Enum(
|
||||
u'confidential',
|
||||
u'public'))
|
||||
|
||||
def generate_identifier(self):
|
||||
self.identifier = unicode(uuid.uuid4())
|
||||
|
||||
def generate_secret(self):
|
||||
self.secret = unicode(
|
||||
bcrypt.hashpw(
|
||||
unicode(uuid.uuid4()),
|
||||
bcrypt.gensalt()))
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0} {1}:{2} ({3})>'.format(
|
||||
self.__class__.__name__,
|
||||
self.id,
|
||||
self.name.encode('ascii', 'replace'),
|
||||
self.owner.username.encode('ascii', 'replace'))
|
||||
|
||||
|
||||
class OAuthUserClient(Base):
|
||||
__tablename__ = 'oauth__user_client'
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
user_id = Column(Integer, ForeignKey(User.id))
|
||||
user = relationship(User, backref='oauth_clients')
|
||||
|
||||
client_id = Column(Integer, ForeignKey(OAuthClient.id))
|
||||
client = relationship(OAuthClient, backref='users')
|
||||
|
||||
state = Column(Enum(
|
||||
u'approved',
|
||||
u'rejected'))
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0} #{1} {2} [{3}, {4}]>'.format(
|
||||
self.__class__.__name__,
|
||||
self.id,
|
||||
self.state.encode('ascii', 'replace'),
|
||||
self.user,
|
||||
self.client)
|
||||
|
||||
|
||||
class OAuthToken(Base):
|
||||
__tablename__ = 'oauth__tokens'
|
||||
@ -39,6 +108,17 @@ class OAuthToken(Base):
|
||||
index=True)
|
||||
user = relationship(User)
|
||||
|
||||
client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False)
|
||||
client = relationship(OAuthClient)
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0} #{1} expires {2} [{3}, {4}]>'.format(
|
||||
self.__class__.__name__,
|
||||
self.id,
|
||||
self.expires.isoformat(),
|
||||
self.user,
|
||||
self.client)
|
||||
|
||||
|
||||
class OAuthCode(Base):
|
||||
__tablename__ = 'oauth__codes'
|
||||
@ -54,5 +134,20 @@ class OAuthCode(Base):
|
||||
index=True)
|
||||
user = relationship(User)
|
||||
|
||||
client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False)
|
||||
client = relationship(OAuthClient)
|
||||
|
||||
MODELS = [OAuthToken, OAuthCode]
|
||||
def __repr__(self):
|
||||
return '<{0} #{1} expires {2} [{3}, {4}]>'.format(
|
||||
self.__class__.__name__,
|
||||
self.id,
|
||||
self.expires.isoformat(),
|
||||
self.user,
|
||||
self.client)
|
||||
|
||||
|
||||
MODELS = [
|
||||
OAuthToken,
|
||||
OAuthCode,
|
||||
OAuthClient,
|
||||
OAuthUserClient]
|
||||
|
31
mediagoblin/plugins/oauth/templates/oauth/authorize.html
Normal file
31
mediagoblin/plugins/oauth/templates/oauth/authorize.html
Normal file
@ -0,0 +1,31 @@
|
||||
{#
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#, se, seee
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-#}
|
||||
{% extends "mediagoblin/base.html" %}
|
||||
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
|
||||
|
||||
{% block mediagoblin_content %}
|
||||
<form action="{{ request.urlgen('mediagoblin.plugins.oauth.authorize_client') }}"
|
||||
method="POST">
|
||||
<div class="form_box_xl">
|
||||
{{ csrf_token }}
|
||||
<h2>Authorize {{ client.name }}?</h2>
|
||||
<p class="client-description">{{ client.description }}</p>
|
||||
{{ wtforms_util.render_divs(form) }}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
@ -0,0 +1,34 @@
|
||||
{#
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-#}
|
||||
{% extends "mediagoblin/base.html" %}
|
||||
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
|
||||
|
||||
{% block mediagoblin_content %}
|
||||
<h1>{% trans %}OAuth client connections{% endtrans %}</h1>
|
||||
{% if connections %}
|
||||
<ul>
|
||||
{% for connection in connections %}
|
||||
<li><span title="{{ connection.client.description }}">{{
|
||||
connection.client.name }}</span> - {{ connection.state }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>You haven't connected using an OAuth client before.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
45
mediagoblin/plugins/oauth/templates/oauth/client/list.html
Normal file
45
mediagoblin/plugins/oauth/templates/oauth/client/list.html
Normal file
@ -0,0 +1,45 @@
|
||||
{#
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-#}
|
||||
{% extends "mediagoblin/base.html" %}
|
||||
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
|
||||
|
||||
{% block mediagoblin_content %}
|
||||
<h1>{% trans %}Your OAuth clients{% endtrans %}</h1>
|
||||
{% if clients %}
|
||||
<ul>
|
||||
{% for client in clients %}
|
||||
<li>{{ client.name }}
|
||||
<dl>
|
||||
<dt>Type</dt>
|
||||
<dd>{{ client.type }}</dd>
|
||||
<dt>Description</dt>
|
||||
<dd>{{ client.description }}</dd>
|
||||
<dt>Identifier</dt>
|
||||
<dd>{{ client.identifier }}</dd>
|
||||
<dt>Secret</dt>
|
||||
<dd>{{ client.secret }}</dd>
|
||||
<dt>Redirect URI<dt>
|
||||
<dd>{{ client.redirect_uri }}</dd>
|
||||
</dl>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>You don't have any clients yet. <a href="{{ request.urlgen('mediagoblin.plugins.oauth.register_client') }}">Add one</a>.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -0,0 +1,34 @@
|
||||
{#
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-#}
|
||||
{% extends "mediagoblin/base.html" %}
|
||||
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
|
||||
|
||||
{% block mediagoblin_content %}
|
||||
<form action="{{ request.urlgen('mediagoblin.plugins.oauth.register_client') }}"
|
||||
method="POST">
|
||||
<div class="form_box_xl">
|
||||
<h1>Register OAuth client</h1>
|
||||
{{ wtforms_util.render_divs(form) }}
|
||||
<div class="form_submit_buttons">
|
||||
{{ csrf_token }}
|
||||
<input type="submit" value="{% trans %}Add{% endtrans %}"
|
||||
class="button_form" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
43
mediagoblin/plugins/oauth/tools.py
Normal file
43
mediagoblin/plugins/oauth/tools.py
Normal file
@ -0,0 +1,43 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from mediagoblin.plugins.oauth.models import OAuthClient
|
||||
from mediagoblin.plugins.api.tools import json_response
|
||||
|
||||
|
||||
def require_client_auth(controller):
|
||||
@wraps(controller)
|
||||
def wrapper(request, *args, **kw):
|
||||
if not request.GET.get('client_id'):
|
||||
return json_response({
|
||||
'status': 400,
|
||||
'errors': [u'No client identifier in URL']},
|
||||
_disable_cors=True)
|
||||
|
||||
client = OAuthClient.query.filter(
|
||||
OAuthClient.identifier == request.GET.get('client_id')).first()
|
||||
|
||||
if not client:
|
||||
return json_response({
|
||||
'status': 400,
|
||||
'errors': [u'No such client identifier']},
|
||||
_disable_cors=True)
|
||||
|
||||
return controller(request, client)
|
||||
|
||||
return wrapper
|
@ -21,38 +21,142 @@ from webob import exc, Response
|
||||
from urllib import urlencode
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
|
||||
from mediagoblin.tools import pluginapi
|
||||
from mediagoblin.tools.response import render_to_response
|
||||
from mediagoblin.tools.response import render_to_response, redirect
|
||||
from mediagoblin.decorators import require_active_login
|
||||
from mediagoblin.messages import add_message, SUCCESS, ERROR
|
||||
from mediagoblin.tools.translate import pass_to_ugettext as _
|
||||
from mediagoblin.plugins.oauth.models import OAuthCode, OAuthToken
|
||||
from mediagoblin.plugins.oauth.models import OAuthCode, OAuthToken, \
|
||||
OAuthClient, OAuthUserClient
|
||||
from mediagoblin.plugins.oauth.forms import ClientRegistrationForm, \
|
||||
AuthorizationForm
|
||||
from mediagoblin.plugins.oauth.tools import require_client_auth
|
||||
from mediagoblin.plugins.api.tools import json_response
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@require_active_login
|
||||
def authorize(request):
|
||||
# TODO: Check if allowed
|
||||
def register_client(request):
|
||||
'''
|
||||
Register an OAuth client
|
||||
'''
|
||||
form = ClientRegistrationForm(request.POST)
|
||||
|
||||
# Client is allowed by the user
|
||||
if True or already_authorized:
|
||||
# Generate a code
|
||||
# Save the code, the client will later use it to obtain an access token
|
||||
# Redirect the user agent to the redirect_uri with the code
|
||||
if request.method == 'POST' and form.validate():
|
||||
client = OAuthClient()
|
||||
client.name = unicode(request.POST['name'])
|
||||
client.description = unicode(request.POST['description'])
|
||||
client.type = unicode(request.POST['type'])
|
||||
client.owner_id = request.user.id
|
||||
client.redirect_uri = unicode(request.POST['redirect_uri'])
|
||||
|
||||
if not 'redirect_uri' in request.GET:
|
||||
add_message(request, ERROR, _('No redirect_uri found'))
|
||||
client.generate_identifier()
|
||||
client.generate_secret()
|
||||
|
||||
client.save()
|
||||
|
||||
add_message(request, SUCCESS, _('The client {0} has been registered!')\
|
||||
.format(
|
||||
client.name))
|
||||
|
||||
return redirect(request, 'mediagoblin.plugins.oauth.list_clients')
|
||||
|
||||
return render_to_response(
|
||||
request,
|
||||
'oauth/client/register.html',
|
||||
{'form': form})
|
||||
|
||||
|
||||
@require_active_login
|
||||
def list_clients(request):
|
||||
clients = request.db.OAuthClient.query.filter(
|
||||
OAuthClient.owner_id == request.user.id).all()
|
||||
return render_to_response(request, 'oauth/client/list.html',
|
||||
{'clients': clients})
|
||||
|
||||
|
||||
@require_active_login
|
||||
def list_connections(request):
|
||||
connections = OAuthUserClient.query.filter(
|
||||
OAuthUserClient.user == request.user).all()
|
||||
return render_to_response(request, 'oauth/client/connections.html',
|
||||
{'connections': connections})
|
||||
|
||||
|
||||
@require_active_login
|
||||
def authorize_client(request):
|
||||
form = AuthorizationForm(request.POST)
|
||||
|
||||
client = OAuthClient.query.filter(OAuthClient.id ==
|
||||
form.client_id.data).first()
|
||||
|
||||
if not client:
|
||||
_log.error('''No such client id as received from client authorization
|
||||
form.''')
|
||||
return exc.HTTPBadRequest()
|
||||
|
||||
if form.validate():
|
||||
relation = OAuthUserClient()
|
||||
relation.user_id = request.user.id
|
||||
relation.client_id = form.client_id.data
|
||||
if form.allow.data:
|
||||
relation.state = u'approved'
|
||||
elif form.deny.data:
|
||||
relation.state = u'rejected'
|
||||
else:
|
||||
return exc.HTTPBadRequest
|
||||
|
||||
relation.save()
|
||||
|
||||
return exc.HTTPFound(location=form.next.data)
|
||||
|
||||
return render_to_response(
|
||||
request,
|
||||
'oauth/authorize.html',
|
||||
{'form': form,
|
||||
'client': client})
|
||||
|
||||
|
||||
@require_client_auth
|
||||
@require_active_login
|
||||
def authorize(request, client):
|
||||
# TODO: Get rid of the JSON responses in this view, it's called by the
|
||||
# user-agent, not the client.
|
||||
user_client_relation = OAuthUserClient.query.filter(
|
||||
(OAuthUserClient.user == request.user)
|
||||
& (OAuthUserClient.client == client))
|
||||
|
||||
if user_client_relation.filter(OAuthUserClient.state ==
|
||||
u'approved').count():
|
||||
redirect_uri = None
|
||||
|
||||
if client.type == u'public':
|
||||
if not client.redirect_uri:
|
||||
return json_response({
|
||||
'status': 400,
|
||||
'errors':
|
||||
[u'Public clients MUST have a redirect_uri pre-set']},
|
||||
_disable_cors=True)
|
||||
|
||||
redirect_uri = client.redirect_uri
|
||||
|
||||
if client.type == u'confidential':
|
||||
redirect_uri = request.GET.get('redirect_uri', client.redirect_uri)
|
||||
if not redirect_uri:
|
||||
return json_response({
|
||||
'status': 400,
|
||||
'errors': [u'Can not find a redirect_uri for client: {0}'\
|
||||
.format(client.name)]}, _disable_cors=True)
|
||||
|
||||
code = OAuthCode()
|
||||
code.code = unicode(uuid4())
|
||||
code.user = request.user
|
||||
code.client = client
|
||||
code.save()
|
||||
|
||||
redirect_uri = ''.join([
|
||||
request.GET.get('redirect_uri'),
|
||||
redirect_uri,
|
||||
'?',
|
||||
urlencode({'code': code.code})])
|
||||
|
||||
@ -65,28 +169,34 @@ def authorize(request):
|
||||
# code parameter
|
||||
# - on deny: send the user agent back to the redirect uri with error
|
||||
# information
|
||||
pass
|
||||
return render_to_response(request, 'oauth/base.html', {})
|
||||
form = AuthorizationForm(request.POST)
|
||||
form.client_id.data = client.id
|
||||
form.next.data = request.url
|
||||
return render_to_response(
|
||||
request,
|
||||
'oauth/authorize.html',
|
||||
{'form': form,
|
||||
'client': client})
|
||||
|
||||
|
||||
def access_token(request):
|
||||
if request.GET.get('code'):
|
||||
code = OAuthCode.query.filter(OAuthCode.code == request.GET.get('code'))\
|
||||
.first()
|
||||
code = OAuthCode.query.filter(OAuthCode.code ==
|
||||
request.GET.get('code')).first()
|
||||
|
||||
if code:
|
||||
token = OAuthToken()
|
||||
token.token = unicode(uuid4())
|
||||
token.user = code.user
|
||||
token.client = code.client
|
||||
token.save()
|
||||
|
||||
access_token_data = {
|
||||
'access_token': token.token,
|
||||
'token_type': 'what_do_i_use_this_for', # TODO
|
||||
'token_type': 'bearer',
|
||||
'expires_in':
|
||||
(token.expires - datetime.now()).total_seconds(),
|
||||
'refresh_token': 'This should probably be safe'}
|
||||
return Response(json.dumps(access_token_data))
|
||||
(token.expires - datetime.now()).total_seconds()}
|
||||
return json_response(access_token_data, _disable_cors=True)
|
||||
|
||||
error_data = {
|
||||
'error': 'Incorrect code'}
|
||||
|
Loading…
x
Reference in New Issue
Block a user