Commit bb1670b1 authored by Thomas Phil's avatar Thomas Phil

RM-54 added support for oauth2 clients_credentials and password grant using authlib

parent 6b27a293
from strongr.core.gateways import Gateways
from strongr.core.domain.schedulerdomain import SchedulerDomain
from strongr.core.domain.restdomain import RestDomain
from .wrapper import Command
......@@ -11,7 +12,8 @@ class MakeDbCommand(Command):
"""
def handle(self):
services = [
SchedulerDomain.schedulerService()
SchedulerDomain.schedulerService(),
RestDomain.oauth2Service()
]
for service in services:
......
......@@ -8,4 +8,4 @@ class RunRestServerCommand(Command):
restdomain:startserver
"""
def handle(self):
Gateways.app().run()
Gateways.flask_app().run()
import dependency_injector.containers as containers
import dependency_injector.providers as providers
from strongr.restdomain.service import Oauth2Service, WsgiService
from strongr.restdomain.service import Oauth2Service
from strongr.restdomain.factory.oauth2 import CommandFactory as Oauth2CommandFactory,\
QueryFactory as Oauth2QueryFactory
from strongr.restdomain.factory.wsgi import QueryFactory as WsgiQueryFactory
class RestDomain(containers.DeclarativeContainer):
"""IoC container"""
oauth2CommandFactory = providers.Singleton(Oauth2CommandFactory)
oauth2QueryFactory = providers.Singleton(Oauth2QueryFactory)
oauth2Service = providers.Singleton(Oauth2Service)
wsgiQueryFactory = providers.Singleton(WsgiQueryFactory)
wsgiService = providers.Singleton(WsgiService)
from sqlalchemy import types, String
import uuid
class UUID(types.TypeDecorator):
impl = String()
def __init__(self, *args, **kwargs):
super(UUID, self).__init__(*args, **kwargs)
self.impl.length = 64
types.TypeDecorator.__init__(self,length=self.impl.length)
def process_bind_param(self,value,dialect=None):
if value and isinstance(value,uuid.UUID):
return value
elif value and not isinstance(value,uuid.UUID):
raise ValueError,'value %s is not a valid uuid.UUID' % value
else:
return None
def process_result_value(self,value,dialect=None):
if value:
return uuid.UUID(value)
else:
return None
def is_mutable(self):
return False
......@@ -2,6 +2,7 @@ from flask import Blueprint
from flask_restplus import Api
from strongr.restdomain.api.v1.scheduler import ns as scheduler_namespace
from strongr.restdomain.api.v1.oauth2 import ns as oauth2_namespace
blueprint = Blueprint('api_v1', __name__, url_prefix='/v1')
api = Api(blueprint,
......@@ -11,3 +12,4 @@ api = Api(blueprint,
)
api.add_namespace(scheduler_namespace)
api.add_namespace(oauth2_namespace)
from functools import wraps
import strongr.restdomain.model.gateways
# oauth2 lib does not support blueprints so we need a hack
# oauth2 lib does not support namespaces so we need a hack
# https://github.com/lepture/flask-oauthlib/issues/180
def blueprint_require_oauth(*scopes):
def namespace_require_oauth(*scopes):
def wrapper(f):
@wraps(f)
def check_oauth(*args, **kwargs):
return app.oauth2.require_oauth(*scopes)(f)(
*args, **kwargs
)
return strongr.restdomain.model.gateways.Gateways.require_oauth()(*scopes)(f)(*args, **kwargs)
return check_oauth
return wrapper
from flask_restplus import Namespace
from flask import url_for, request
from flask_restplus import Namespace, Resource
import strongr.restdomain.model.gateways
from strongr.restdomain.model.oauth2 import Client
ns = Namespace('oauth', description='Operations related to oauth2 login')
@ns.route('/revoke', methods=['POST'])
class Revoke(Resource):
def post(self):
auth_server = strongr.restdomain.model.gateways.Gateways.auth_server()
return auth_server.create_revocation_response()
@ns.route('/login')
def login():
callback_uri = url_for('.authorize', _external=True)
return oauth.twitter.authorize_redirect(callback_uri)
@ns.route('/token', methods=['POST'])
class Token(Resource):
def post(self):
auth_server = strongr.restdomain.model.gateways.Gateways.auth_server()
return auth_server.create_token_response()
@ns.route('/authorize')
def authorize():
token = oauth.twitter.authorize_access_token()
# this is a pseudo method, you need to implement it yourself
MyTokenModel.save(token)
return redirect('/profile')
# @ns.route('/authorize')
# class Authorize(Resource):
# def get(self):
# auth_server = strongr.restdomain.model.gateways.Gateways.auth_server()
# # Login is required since we need to know the current resource owner.
# # It can be done with a redirection to the login page, or a login
# # form on this authorization page.
# if request.method == 'GET':
# grant = auth_server.validate_authorization_request()
# #return render_template(
# # 'authorize.html',
# # grant=grant,
# # user=current_user,
# #)
# confirmed = request.form['confirm']
# if confirmed:
# # granted by resource owner
# return auth_server.create_authorization_response()
# # denied by resource owner
# return auth_server.create_authorization_response(None)
#
# def post(self):
# auth_server = strongr.restdomain.model.gateways.Gateways.auth_server()
# # Login is required since we need to know the current resource owner.
# # It can be done with a redirection to the login page, or a login
# # form on this authorization page.
# #if request.method == 'GET':
# #grant = auth_server.validate_authorization_request()
# # return render_template(
# # 'authorize.html',
# # grant=grant,
# # user=current_user,
# # )
# #confirmed = request.form['confirm']
# #if confirmed:
# # granted by resource owner
# return auth_server.create_authorization_response('1')
# # denied by resource owner
# #return auth_server.create_authorization_response(None)
from flask_restplus import Namespace, Resource, fields, reqparse
from flask import request
from strongr.restdomain.api.utils import blueprint_require_oauth
import strongr.core
import time
import uuid
import strongr.restdomain.model.gateways
from strongr.core.domain.schedulerdomain import SchedulerDomain
from strongr.restdomain.api.utils import namespace_require_oauth
ns = Namespace('scheduler', description='Operations related to the schedulerdomain')
post_task = ns.model('post-task', {
......@@ -17,12 +19,16 @@ post_task = ns.model('post-task', {
@ns.route('/task')
class Tasks(Resource):
def __init__(self, *args, **kwargs):
super(Tasks, self).__init__(*args, **kwargs)
@ns.response(200, 'OK')
@namespace_require_oauth('task')
@ns.param('task_id')
def get(self):
"""Requests task status"""
schedulerService = strongr.core.getCore().domains().schedulerDomain().schedulerService()
queryFactory = strongr.core.getCore().domains().schedulerDomain().queryFactory()
schedulerService = SchedulerDomain.schedulerService()
queryFactory = SchedulerDomain.queryFactory()
query = queryFactory.newRequestScheduledJobs()
......@@ -30,12 +36,12 @@ class Tasks(Resource):
return result, 200
@ns.response(201, 'Task successfully created.')
@blueprint_require_oauth('task')
@namespace_require_oauth('task')
@ns.expect(post_task, validate=True)
def post(self):
"""Creates a new task."""
schedulerService = strongr.core.getCore().domains().schedulerDomain().schedulerService()
commandFactory = strongr.core.getCore().domains().schedulerDomain().commandFactory()
schedulerService = SchedulerDomain.schedulerService()
commandFactory = SchedulerDomain.commandFactory()
cmd = request.json['cmd']
cores = int(request.json['cores'])
......
......@@ -2,15 +2,23 @@ import dependency_injector.containers as containers
import dependency_injector.providers as providers
from flask import Flask
from authlib.flask.oauth2 import AuthorizationServer
from werkzeug.security import generate_password_hash, check_password_hash
from authlib.flask.oauth2 import AuthorizationServer, ResourceProtector
from strongr.restdomain.api.apiv1 import blueprint as apiv1
from strongr.restdomain.model.oauth2 import Token
from strongr.restdomain.model.oauth2.client import Client
import strongr.core
from strongr.restdomain.model.oauth2.endpoints.revocationendpoint import RevocationEndpoint
from strongr.restdomain.model.oauth2.grants.authorizationcodegrant import AuthorizationCode
from strongr.restdomain.model.oauth2.grants.clientcredentialsgrant import ClientCredentialsGrant
from strongr.restdomain.model.oauth2.grants.passwordgrant import PasswordGrant
class Gateways(containers.DeclarativeContainer):
"""IoC container of gateway objects."""
_backends = providers.Object({
})
_blueprints = providers.Object([apiv1])
def _factor_app(name, blueprints):
......@@ -31,7 +39,15 @@ class Gateways(containers.DeclarativeContainer):
for blueprint in blueprints:
app.register_blueprint(blueprint)
server = AuthorizationServer(Client, app)
auth_server = AuthorizationServer(Client, app)
auth_server.register_grant_endpoint(AuthorizationCode)
auth_server.register_grant_endpoint(PasswordGrant)
auth_server.register_grant_endpoint(ClientCredentialsGrant)
auth_server.register_revoke_token_endpoint(RevocationEndpoint)
Gateways.auth_server.override(auth_server)
if backend == 'flask':
flask_config = config.restdomain.flask.as_dict() if hasattr(config, 'restdomain') and hasattr(config.restdomain, 'flask') else {}
......@@ -58,5 +74,21 @@ class Gateways(containers.DeclarativeContainer):
return WSGIServer(app)
@classmethod
def _query_token(self, access_token):
from authlib.flask.oauth2 import current_token
import strongr.core.gateways
return strongr.core.gateways.Gateways.sqlalchemy_session().query(Token).filter_by(access_token=access_token).first()
def _factor_require_oauth():
return ResourceProtector(Gateways._query_token)
flask_app = providers.Singleton(_factor_app, 'StrongRRestServer', _blueprints())
auth_server = providers.Configuration('auth_server') # initialized by _factor_app factory
#current_user = providers.Factory(_factor_current_user)
require_oauth = providers.Factory(_factor_require_oauth)
app = providers.Singleton(_factor_app, 'StrongRRestServer', _blueprints())
generate_password_hash = providers.Factory(generate_password_hash, method='pbkdf2:sha256:80000', salt_length=8)
check_password_hash = providers.Factory(check_password_hash)
from .user import User
from .token import Token
from .client import Client
from .authorizationcode import AuthorizationCode
from authlib.flask.oauth2.sqla import OAuth2AuthorizationCodeMixin
from strongr.core import gateways
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from strongr.core.sqlalchemydatatypes.uuid import UUID
Base = gateways.Gateways.sqlalchemy_base()
class AuthorizationCode(Base, OAuth2AuthorizationCodeMixin):
__tablename__ = 'oauth_auth_code'
authorizationcode_id = Column(Integer, primary_key=True)
user_id = Column(String(64), ForeignKey('oauth_users.user_id', ondelete='CASCADE'))
from sqlalchemy.orm import relationship
import strongr.core.gateways as gateways
from sqlalchemy import Column, ForeignKey, Integer
from sqlalchemy import Column, ForeignKey, Integer, String
from authlib.flask.oauth2.sqla import OAuth2ClientMixin
from strongr.core.sqlalchemydatatypes.uuid import UUID
Base = gateways.Gateways.sqlalchemy_base()
class Client(Base, OAuth2ClientMixin):
__tablename__ = 'oauth_client'
id = Column(Integer, primary_key=True)
user_id = Column(
Integer, ForeignKey('user.id', ondelete='CASCADE')
)
user = relationship('User')
id = Column(Integer, primary_key=True) # client_id is provided by authlib, can't use it here
user_id = Column(String(64), ForeignKey('oauth_users.user_id', ondelete='CASCADE'))
@classmethod
def get_by_client_id(cls, client_id): # this method in OAuth2ClientMixin is bugged in authlib, thus we must patch it
client = gateways.Gateways.sqlalchemy_session().query(Client).filter_by(client_id=client_id).first()
return client
from authlib.specs.rfc7009 import RevocationEndpoint as _RevocationEndpoint
from strongr.core.gateways import Gateways
from strongr.restdomain.model.oauth2 import Token
class RevocationEndpoint(_RevocationEndpoint):
def query_token(self, token, token_type_hint, client):
q = Token.query.filter_by(client_id=client.client_id)
if token_type_hint == 'access_token':
return q.filter_by(access_token=token).first()
elif token_type_hint == 'refresh_token':
return q.filter_by(refresh_token=token).first()
# without token_type_hint
item = q.filter_by(access_token=token).first()
if item:
return item
return q.filter_by(refresh_token=token).first()
def invalidate_token(self, token):
session = Gateways.sqlalchemy_session()
session.delete(token)
session.commit()
from authlib.specs.rfc6749.grants import (
AuthorizationCodeGrant as _AuthorizationCodeGrant
)
from authlib.common.security import generate_token
from strongr.core.gateways import Gateways
from strongr.restdomain.model.oauth2 import Token
from strongr.restdomain.model.oauth2.authorizationcode import AuthorizationCode
class AuthorizationCode(_AuthorizationCodeGrant):
def create_authorization_code(self, client, grant_user, **kwargs):
# you can use other method to generate this code
code = generate_token(48)
item = AuthorizationCode(
code=code,
client_id=client.client_id,
redirect_uri=kwargs.get('redirect_uri', ''),
scope=kwargs.get('scope', ''),
user_id=grant_user,
)
session = Gateways.sqlalchemy_session()
session.add(item)
session.commit()
return code
def parse_authorization_code(self, code, client):
item = AuthorizationCode.query.filter_by(
code=code, client_id=client.client_id).first()
if item and not item.is_expired():
return item
def delete_authorization_code(self, authorization_code):
session = Gateways.sqlalchemy_session()
session.delete(authorization_code)
session.commit()
def create_access_token(self, token, client, authorization_code):
item = Token(
client_id=client.client_id,
user_id=authorization_code.user_id,
**token
)
session = Gateways.sqlalchemy_session()
session.add(item)
session.commit()
# we can add more data into token
token['user_id'] = authorization_code.user_id
from authlib.specs.rfc6749.grants import (
ClientCredentialsGrant as _ClientCredentialsGrant
)
from strongr.core.gateways import Gateways
from strongr.restdomain.model.oauth2 import Token
class ClientCredentialsGrant(_ClientCredentialsGrant):
def create_access_token(self, token, client):
item = Token(
client_id=client.client_id,
user_id=client.user_id,
**token
)
session = Gateways.sqlalchemy_session()
session.add(item)
session.commit()
from authlib.specs.rfc6749.grants import (
ResourceOwnerPasswordCredentialsGrant as _PasswordGrant
)
from strongr.core.gateways import Gateways
from strongr.restdomain.model.oauth2 import User, Token
class PasswordGrant(_PasswordGrant):
def authenticate_user(self, username, password):
user = User.query.filter_by(username=username).first()
if user.check_password(password):
return user
def create_access_token(self, token, client, user, **kwargs):
item = Token(
client_id=client.client_id,
user_id=user.user_id,
**token
)
session = Gateways.sqlalchemy_session()
session.add(item)
session.commit()
from authlib.specs.rfc6749.grants import (
RefreshTokenGrant as _RefreshTokenGrant
)
from strongr.core.gateways import Gateways
from strongr.restdomain.model.oauth2 import Token
class RefreshTokenGrant(_RefreshTokenGrant):
def authenticate_token(self, refresh_token):
item = Token.query.filter_by(refresh_token=refresh_token).first()
# define is_refresh_token_expired by yourself
if item and not item.is_refresh_token_expired():
return item
def create_access_token(self, token, authenticated_token):
# issue a new token to replace the old one, you can also update
# the ``authenticated_token`` instead of issuing a new one
item = Token(
client_id=authenticated_token.client_id,
user_id=authenticated_token.user_id,
**token
)
session = Gateways.sqlalchemy_session()
session.add(item)
session.delete(authenticated_token)
session.commit()
import strongr.core.gateways as gateways
from sqlalchemy import Column, ForeignKey, Integer
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from authlib.flask.oauth2.sqla import OAuth2TokenMixin
from strongr.core.sqlalchemydatatypes.uuid import UUID
Base = gateways.Gateways.sqlalchemy_base()
......@@ -10,7 +11,4 @@ class Token(Base, OAuth2TokenMixin):
__tablename__ = 'oauth_token'
id = Column(Integer, primary_key=True)
user_id = Column(
Integer, ForeignKey('user.id', ondelete='CASCADE')
)
user = relationship('User')
user_id = Column(String(64), ForeignKey('oauth_users.user_id', ondelete='CASCADE'))
import uuid
from sqlalchemy.orm import synonym
import strongr.core.gateways as gateways
from sqlalchemy import Column, String
from strongr.core.sqlalchemydatatypes.uuid import UUID
import strongr.restdomain.model.gateways
Base = gateways.Gateways.sqlalchemy_base()
class User(Base):
__tablename__ = 'oauth_users'
user_id = Column(String(64), primary_key=True, nullable=False, default=uuid.uuid4)
username = Column(String(64), nullable=False)
_password = Column('password', String(93)) # werkzeug generate_password_hash method='pbkdf2:sha256:80000' == 93 chars
email = Column(String(250), nullable=False)
@property
def password(self):
return self._password
# update password-field using proper hash function
@password.setter
def password(self, value):
self._password = strongr.restdomain.model.gateways.Gateways.generate_password_hash(password=value)
def check_password(self, password):
return strongr.restdomain.model.gateways.Gateways.check_password_hash(pwhash=self.password, password=password)
# create a synonym so that _password and password are considered the same field by the mapper
password = synonym('_password', descriptor=password)
from .oauth2service import Oauth2Service
from .wsgiservice import WsgiService
......@@ -14,7 +14,7 @@ class Oauth2Service(AbstractService):
_query_bus = None
def register_models(self):
import strongr.restdomain.model
import strongr.restdomain.model.oauth2
# importing alone is enough for registration
def getCommandBus(self):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment