Source code for hawat.blueprints.auth_api

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# This file is part of Mentat system (https://mentat.cesnet.cz/).
#
# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/)
# Use of this source is governed by the MIT license, see LICENSE file.
#-------------------------------------------------------------------------------


"""
Description
--------------------------------------------------------------------------------

This pluggable module provides API key based authentication service. When this
module is enabled, users may generate and use API keys to authenticate themselves
when accessing various API application endpoints.

Currently the API key may be provided via one of the following methods:

* The ``Authorization`` HTTP header.

  You may provide your API key by adding ``Authorization`` HTTP header to your
  requests. Following forms are accepted::

    Authorization: abcd1234
    Authorization: key abcd1234
    Authorization: token abcd1234

* The ``api_key`` or ``api_token`` parameter of the HTTP ``POST`` request.

  You may provide your API key as additional HTTP parameter ``api_key`` or
  ``api_token`` of your ``POST`` request to particular application endpoint.
  Using ``GET`` requests is forbidden due to the fact that request URLs are getting
  logged on various places and your keys could thus be easily compromised.


Provided endpoints
--------------------------------------------------------------------------------

``/auth_api/<user_id>/key-generate``
    Page enabling generation of new API key.

    * *Authentication:* login required
    * *Authorization:* ``admin``
    * *Methods:* ``GET``, ``POST``

``/auth_api/<user_id>/key-delete``
    Page enabling deletion of existing API key.

    * *Authentication:* login required
    * *Authorization:* ``admin``
    * *Methods:* ``GET``, ``POST``
"""


__author__ = "Jan Mach <jan.mach@cesnet.cz>"
__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"


import itsdangerous

#
# Flask related modules.
#
import flask
import flask_login
import flask_principal
from flask_babel import gettext, lazy_gettext

#
# Custom modules.
#
from mentat.datatype.sqldb import UserModel
import hawat.const
import hawat.db
import hawat.forms
from hawat.base import HTMLMixin, SQLAlchemyMixin, ItemChangeView, HawatBlueprint


BLUEPRINT_NAME = 'auth_api'
"""Name of the blueprint as module global constant."""


[docs]class GenerateKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: disable=locally-disabled,too-many-ancestors """ View for generating API keys for user accounts. """ methods = ['GET','POST'] authentication = True authorization = [hawat.acl.PERMISSION_ADMIN]
[docs] @classmethod def get_view_name(cls): """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" return 'key-generate'
[docs] @classmethod def get_view_icon(cls): """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" return 'action-genkey'
[docs] @classmethod def get_view_title(cls, **kwargs): """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" return lazy_gettext('Generate API key')
[docs] @classmethod def get_view_template(cls): """*Implementation* of :py:func:`hawat.base.RenderableView.get_view_template`.""" return 'auth_api/key-generate.html'
#--------------------------------------------------------------------------- @property def dbmodel(self): """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" return UserModel #---------------------------------------------------------------------------
[docs] @staticmethod def get_message_success(**kwargs): """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" return gettext( 'API key for user account <strong>%(item_id)s</strong> was successfully generated.', item_id = str(kwargs['item']) )
[docs] @staticmethod def get_message_failure(**kwargs): """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" return gettext( 'Unable to generate API key for user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item']) )
[docs] @staticmethod def get_message_cancel(**kwargs): """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" return gettext( 'Canceled generating API key for user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item']) )
[docs] @classmethod def change_item(cls, item): """ *Interface implementation* of :py:func:`hawat.base.ItemChangeView.change_item`. """ serializer = itsdangerous.URLSafeTimedSerializer( flask.current_app.config['SECRET_KEY'], salt = 'apikey-user' ) item.apikey = serializer.dumps(item.id)
[docs]class DeleteKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: disable=locally-disabled,too-many-ancestors """ View for deleting API keys from user accounts. """ methods = ['GET','POST'] authentication = True authorization = [hawat.acl.PERMISSION_ADMIN]
[docs] @classmethod def get_view_name(cls): """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" return 'key-delete'
[docs] @classmethod def get_view_icon(cls): """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" return 'action-delete'
[docs] @classmethod def get_view_title(cls, **kwargs): """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" return lazy_gettext('Delete API key')
[docs] @classmethod def get_view_template(cls): """*Implementation* of :py:func:`hawat.base.RenderableView.get_view_template`.""" return 'auth_api/key-delete.html'
#--------------------------------------------------------------------------- @property def dbmodel(self): """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" return UserModel #---------------------------------------------------------------------------
[docs] @staticmethod def get_message_success(**kwargs): """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" return gettext( 'API key for user account <strong>%(item_id)s</strong> was successfully deleted.', item_id = str(kwargs['item']) )
[docs] @staticmethod def get_message_failure(**kwargs): """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" return gettext( 'Unable to delete API key for user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item']) )
[docs] @staticmethod def get_message_cancel(**kwargs): """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" return gettext( 'Canceled deleting API key for user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item']) )
[docs] @classmethod def change_item(cls, item): """ *Interface implementation* of :py:func:`hawat.base.ItemChangeView.change_item`. """ item.apikey = None
#-------------------------------------------------------------------------------
[docs]class APIAuthBlueprint(HawatBlueprint): """ Hawat pluggable module - API key based authentication (*auth_api*). """
[docs] @classmethod def get_module_title(cls): """*Implementation* of :py:func:`hawat.base.HawatBlueprint.get_module_title`.""" return gettext('API key authentication service')
[docs] def register_app(self, app): """ *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization :param hawat.base.HawatApp app: Flask application to be customize. """ login_manager = app.get_resource(hawat.const.RESOURCE_LOGIN_MANAGER) principal = app.get_resource(hawat.const.RESOURCE_PRINCIPAL) @login_manager.request_loader def load_user_from_request(request): # pylint: disable=locally-disabled,unused-variable """ Custom login callback for login via request object. https://flask-login.readthedocs.io/en/latest/#custom-login-using-request-loader """ # Attempt to extract token from Authorization header. Following formats # may be used: # Authorization: abcd1234 # Authorization: key abcd1234 # Authorization: token abcd1234 api_key = request.headers.get("Authorization") if api_key: vals = api_key.split() if len(vals) == 1: api_key = vals[0] elif len(vals) == 2 and vals[0] in ("token", "key"): api_key = vals[1] else: api_key = None # API key may also be received via POST method, parameters 'api_key' # or 'api_token'. The GET method is forbidden due to the lack # of security, there is a possiblity for it to be stored in various # insecure places like web server logs. if not api_key: api_key = request.form.get('api_key') if not api_key: api_key = request.form.get('api_token') # Now login the user with provided API key. if api_key: dbsess = hawat.db.db_get().session try: user = dbsess.query(UserModel).filter(UserModel.apikey == api_key).one() if user: if user.enabled: flask.current_app.logger.info( "User '{}' used API key to access the resource '{}'.".format(user.login, request.url) ) return user flask.current_app.logger.error( "The API key for user account '{}' was rejected, the account is disabled.".format(user.login) ) except: # pylint: disable=locally-disabled,bare-except pass # Return ``None`` if API key method did not login the user. return None @flask_login.user_loaded_from_request.connect_via(app) def on_user_loaded_from_request(sender, user): # pylint: disable=locally-disabled,unused-variable, unused-argument """ Without whis intermediate step the flask_principal.on_identity_loaded callback is never called and the user identity does not have the correct permissions set. Solution resource: https://github.com/mattupstate/flask-principal/issues/22#issuecomment-145897838 """ principal.set_identity( flask_principal.Identity(user.id) )
#-------------------------------------------------------------------------------
[docs]def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid instance of :py:class:`hawat.base.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = APIAuthBlueprint( BLUEPRINT_NAME, __name__, template_folder = 'templates', url_prefix = '/{}'.format(BLUEPRINT_NAME) ) hbp.register_view_class(GenerateKeyView, '/<int:item_id>/key-generate') hbp.register_view_class(DeleteKeyView, '/<int:item_id>/key-delete') return hbp