Add OAuth models, plugin DB migrations, api_auth
This commit is contained in:
parent
bc875dc7cc
commit
f46e2a4db9
@ -19,6 +19,7 @@ from sqlalchemy import create_engine
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from mediagoblin.db.sql.base import Base, Session
|
from mediagoblin.db.sql.base import Base, Session
|
||||||
|
from mediagoblin import mg_globals
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -51,10 +52,18 @@ class DatabaseMaster(object):
|
|||||||
def load_models(app_config):
|
def load_models(app_config):
|
||||||
import mediagoblin.db.sql.models
|
import mediagoblin.db.sql.models
|
||||||
|
|
||||||
if True:
|
for media_type in app_config['media_types']:
|
||||||
for media_type in app_config['media_types']:
|
_log.debug("Loading %s.models", media_type)
|
||||||
_log.debug("Loading %s.models", media_type)
|
__import__(media_type + ".models")
|
||||||
__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):
|
def setup_connection_and_db_from_config(app_config):
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from mediagoblin.db.sql.open import setup_connection_and_db_from_config
|
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.init import setup_global_and_app_config
|
||||||
from mediagoblin.tools.common import import_component
|
from mediagoblin.tools.common import import_component
|
||||||
|
|
||||||
|
_log = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig()
|
||||||
|
_log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
def dbupdate_parse_setup(subparser):
|
def dbupdate_parse_setup(subparser):
|
||||||
pass
|
pass
|
||||||
@ -37,7 +42,7 @@ class DatabaseData(object):
|
|||||||
self.name, self.models, self.migrations, session)
|
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
|
Gather all database data relevant to the extensions we have
|
||||||
installed so we can do migrations and table initialization.
|
installed so we can do migrations and table initialization.
|
||||||
@ -61,10 +66,41 @@ def gather_database_data(media_types):
|
|||||||
managed_dbdata.append(
|
managed_dbdata.append(
|
||||||
DatabaseData(media_type, models, migrations))
|
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
|
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.
|
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
|
# 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
|
# Set up the database
|
||||||
connection, db = setup_connection_and_db_from_config(app_config)
|
connection, db = setup_connection_and_db_from_config(app_config)
|
||||||
@ -89,4 +127,4 @@ def run_dbupdate(app_config):
|
|||||||
|
|
||||||
def dbupdate(args):
|
def dbupdate(args):
|
||||||
global_config, app_config = setup_global_and_app_config(args.conf_file)
|
global_config, app_config = setup_global_and_app_config(args.conf_file)
|
||||||
run_dbupdate(app_config)
|
run_dbupdate(app_config, global_config)
|
||||||
|
90
mediagoblin/plugins/oauth/__init__.py
Normal file
90
mediagoblin/plugins/oauth/__init__.py
Normal 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()
|
||||||
|
}
|
58
mediagoblin/plugins/oauth/models.py
Normal file
58
mediagoblin/plugins/oauth/models.py
Normal 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]
|
105
mediagoblin/plugins/oauth/views.py
Normal file
105
mediagoblin/plugins/oauth/views.py
Normal 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))
|
@ -29,7 +29,7 @@ How do plugins work?
|
|||||||
====================
|
====================
|
||||||
|
|
||||||
Plugins are structured like any Python project. You create a Python package.
|
Plugins are structured like any Python project. You create a Python package.
|
||||||
In that package, you define a high-level ``__init__.py`` module that has a
|
In that package, you define a high-level ``__init__.py`` module that has a
|
||||||
``hooks`` dict that maps hooks to callables that implement those hooks.
|
``hooks`` dict that maps hooks to callables that implement those hooks.
|
||||||
|
|
||||||
Additionally, you want a LICENSE file that specifies the license and a
|
Additionally, you want a LICENSE file that specifies the license and a
|
||||||
@ -58,6 +58,8 @@ Lifecycle
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
from mediagoblin import mg_globals
|
from mediagoblin import mg_globals
|
||||||
|
|
||||||
|
|
||||||
@ -205,3 +207,34 @@ def get_config(key):
|
|||||||
global_config = mg_globals.global_config
|
global_config = mg_globals.global_config
|
||||||
plugin_section = global_config.get('plugins', {})
|
plugin_section = global_config.get('plugins', {})
|
||||||
return plugin_section.get(key, {})
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user