Add OAuth models, plugin DB migrations, api_auth

This commit is contained in:
Joar Wandborg 2012-09-12 22:41:04 +02:00
parent bc875dc7cc
commit f46e2a4db9
6 changed files with 342 additions and 9 deletions

View File

@ -19,6 +19,7 @@ from sqlalchemy import create_engine
import logging
from mediagoblin.db.sql.base import Base, Session
from mediagoblin import mg_globals
_log = logging.getLogger(__name__)
@ -51,10 +52,18 @@ class DatabaseMaster(object):
def load_models(app_config):
import mediagoblin.db.sql.models
if True:
for media_type in app_config['media_types']:
_log.debug("Loading %s.models", media_type)
__import__(media_type + ".models")
for media_type in app_config['media_types']:
_log.debug("Loading %s.models", media_type)
__import__(media_type + ".models")
for plugin in mg_globals.global_config['plugins'].keys():
_log.debug("Loading %s.models", plugin)
try:
__import__(plugin + ".models")
except ImportError as exc:
_log.debug("Could not load {0}.models: {1}".format(
plugin,
exc))
def setup_connection_and_db_from_config(app_config):

View File

@ -14,6 +14,8 @@
# 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 logging
from sqlalchemy.orm import sessionmaker
from mediagoblin.db.sql.open import setup_connection_and_db_from_config
@ -21,6 +23,9 @@ from mediagoblin.db.sql.util import MigrationManager
from mediagoblin.init import setup_global_and_app_config
from mediagoblin.tools.common import import_component
_log = logging.getLogger(__name__)
logging.basicConfig()
_log.setLevel(logging.DEBUG)
def dbupdate_parse_setup(subparser):
pass
@ -37,7 +42,7 @@ class DatabaseData(object):
self.name, self.models, self.migrations, session)
def gather_database_data(media_types):
def gather_database_data(media_types, plugins):
"""
Gather all database data relevant to the extensions we have
installed so we can do migrations and table initialization.
@ -61,10 +66,41 @@ def gather_database_data(media_types):
managed_dbdata.append(
DatabaseData(media_type, models, migrations))
for plugin in plugins:
try:
models = import_component('{0}.models:MODELS'.format(plugin))
except ImportError as exc:
_log.debug('No models found for {0}: {1}'.format(
plugin,
exc))
models = []
except AttributeError as exc:
_log.warning('Could not find MODELS in {0}.models, have you \
forgotten to add it? ({1})'.format(plugin, exc))
try:
migrations = import_component('{0}.migrations:MIGRATIONS'.format(
plugin))
except ImportError as exc:
_log.debug('No migrations found for {0}: {1}'.format(
plugin,
exc))
migrations = {}
except AttributeError as exc:
_log.debug('Cloud not find MIGRATIONS in {0}.migrations, have you \
forgotten to add it? ({1})'.format(plugin, exc))
if models:
managed_dbdata.append(
DatabaseData(plugin, models, migrations))
return managed_dbdata
def run_dbupdate(app_config):
def run_dbupdate(app_config, global_config):
"""
Initialize or migrate the database as specified by the config file.
@ -73,7 +109,9 @@ def run_dbupdate(app_config):
"""
# Gather information from all media managers / projects
dbdatas = gather_database_data(app_config['media_types'])
dbdatas = gather_database_data(
app_config['media_types'],
global_config['plugins'].keys())
# Set up the database
connection, db = setup_connection_and_db_from_config(app_config)
@ -89,4 +127,4 @@ def run_dbupdate(app_config):
def dbupdate(args):
global_config, app_config = setup_global_and_app_config(args.conf_file)
run_dbupdate(app_config)
run_dbupdate(app_config, global_config)

View File

@ -0,0 +1,90 @@
# 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 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
_log = logging.getLogger(__name__)
PLUGIN_DIR = os.path.dirname(__file__)
def setup_plugin():
config = pluginapi.get_config('mediagoblin.plugins.oauth')
_log.info('Setting up OAuth...')
_log.debug('OAuth config: {0}'.format(config))
routes = [
Route('mediagoblin.plugins.oauth.authorize', '/oauth/authorize',
controller='mediagoblin.plugins.oauth.views:authorize'),
Route('mediagoblin.plugins.oauth.test', '/api/test',
controller='mediagoblin.plugins.oauth.views:api_test'),
Route('mediagoblin.plugins.oauth.access_token', '/oauth/access_token',
controller='mediagoblin.plugins.oauth.views:access_token')]
pluginapi.register_routes(routes)
pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
class OAuthAuth(object):
'''
An object with two significant methods, 'trigger' and 'run'.
Using a similar object to this, plugins can register specific
authentication logic, for example the GET param 'access_token' for OAuth.
- trigger: Analyze the 'request' argument, return True if you think you
can handle the request, otherwise return False
- run: The authentication logic, set the request.user object to the user
you intend to authenticate and return True, otherwise return False.
If run() returns False, an HTTP 403 Forbidden error will be shown.
You may also display custom errors, just raise them within the run()
method.
'''
def __init__(self):
pass
def trigger(self, request):
return True
def __call__(self, request, *args, **kw):
access_token = request.GET.get('access_token')
if access_token:
token = OAuthToken.query.filter(OAuthToken.token == access_token)\
.first()
if not token:
return False
request.user = token.user
return True
hooks = {
'setup': setup_plugin,
'auth': OAuthAuth()
}

View File

@ -0,0 +1,58 @@
# 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 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)
from sqlalchemy.orm import relationship
class OAuthToken(Base):
__tablename__ = 'oauth__tokens'
id = Column(Integer, primary_key=True)
created = Column(DateTime, nullable=False,
default=datetime.now)
expires = Column(DateTime, nullable=False,
default=lambda: datetime.now() + timedelta(days=30))
token = Column(Unicode, index=True)
refresh_token = Column(Unicode, index=True)
user_id = Column(Integer, ForeignKey(User.id), nullable=False,
index=True)
user = relationship(User)
class OAuthCode(Base):
__tablename__ = 'oauth__codes'
id = Column(Integer, primary_key=True)
created = Column(DateTime, nullable=False,
default=datetime.now)
expires = Column(DateTime, nullable=False,
default=lambda: datetime.now() + timedelta(minutes=5))
code = Column(Unicode, index=True)
user_id = Column(Integer, ForeignKey(User.id), nullable=False,
index=True)
user = relationship(User)
MODELS = [OAuthToken, OAuthCode]

View File

@ -0,0 +1,105 @@
# 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 logging
import json
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.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
_log = logging.getLogger(__name__)
@require_active_login
def authorize(request):
# TODO: Check if allowed
# 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 not 'redirect_uri' in request.GET:
add_message(request, ERROR, _('No redirect_uri found'))
code = OAuthCode()
code.code = unicode(uuid4())
code.user = request.user
code.save()
redirect_uri = ''.join([
request.GET.get('redirect_uri'),
'?',
urlencode({'code': code.code})])
_log.debug('Redirecting to {0}'.format(redirect_uri))
return exc.HTTPFound(location=redirect_uri)
else:
# Show prompt to allow client to access data
# - on accept: send the user agent back to the redirect_uri with the
# 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', {})
def access_token(request):
if request.GET.get('code'):
code = OAuthCode.query.filter(OAuthCode.code == request.GET.get('code'))\
.first()
if code:
token = OAuthToken()
token.token = unicode(uuid4())
token.user = code.user
token.save()
access_token_data = {
'access_token': token.token,
'token_type': 'what_do_i_use_this_for', # TODO
'expires_in':
(token.expires - datetime.now()).total_seconds(),
'refresh_token': 'This should probably be safe'}
return Response(json.dumps(access_token_data))
error_data = {
'error': 'Incorrect code'}
return Response(json.dumps(error_data))
@pluginapi.api_auth
def api_test(request):
if not request.user:
return exc.HTTPForbidden()
user_data = {
'username': request.user.username,
'email': request.user.email}
return Response(json.dumps(user_data))

View File

@ -58,6 +58,8 @@ Lifecycle
import logging
from functools import wraps
from mediagoblin import mg_globals
@ -205,3 +207,34 @@ def get_config(key):
global_config = mg_globals.global_config
plugin_section = global_config.get('plugins', {})
return plugin_section.get(key, {})
def api_auth(controller):
@wraps(controller)
def wrapper(request, *args, **kw):
auth_candidates = []
for auth in PluginManager().get_hook_callables('auth'):
_log.debug('Plugin auth: {0}'.format(auth))
if auth.trigger(request):
auth_candidates.append(auth)
# If we can't find any authentication methods, we should not let them
# pass.
if not auth_candidates:
from webob import exc
return exc.HTTPForbidden()
# For now, just select the first one in the list
auth = auth_candidates[0]
_log.debug('Using {0} to authorize request {1}'.format(
auth, request.url))
if not auth(request, *args, **kw):
from webob import exc
return exc.HTTPForbidden()
return controller(request, *args, **kw)
return wrapper