From e7b455f88009ae20b8b88fe1a1515866f84b939c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 19 Oct 2017 13:15:18 +0200 Subject: [PATCH] New hook octoprint.accesscontrol.keyvalidator Allows plugins to provide their own API keys which then can be validated/translated into a User instance via this hook. --- docs/plugins/hooks.rst | 31 +++++++++++++++++++ src/octoprint/server/util/__init__.py | 44 ++++++++++++++++++++------- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/docs/plugins/hooks.rst b/docs/plugins/hooks.rst index a53a4781..75c255da 100644 --- a/docs/plugins/hooks.rst +++ b/docs/plugins/hooks.rst @@ -226,6 +226,37 @@ octoprint.accesscontrol.appkey :return: A list of 3-tuples as described above :rtype: list +.. _sec-plugins-hook-accesscontrol-keyvalidator: + +octoprint.accesscontrol.keyvalidator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. py:function:: acl_keyvalidator_hook(apikey, *args, **kwargs) + + Via this hook plugins may validate their own customized API keys to be used to access OctoPrint's API. + + ``apikey`` will be the API key as read from the request headers. + + Hook handlers are expected to return a :class:`~octoprint.users.User` instance here that will then be considered that + user making the request. By returning ``None`` or nothing at all, hook handlers signal that they do not handle the + provided key. + + **Example:** + + Allows using a user's id as their API key (for obvious reasons this is NOT recommended in production environments + and merely provided for educational purposes): + + .. onlineinclude:: https://raw.githubusercontent.com/OctoPrint/Plugin-Examples/master/custom_keyvalidator.py + :linenos: + :tab-width: 4 + :caption: `custom_keyvalidator.py `_ + + .. versionadded:: 1.3.6 + + :param str apikey: The API key to validate + :return: The user in whose name the request will be processed further + :rtype: :class:`~octoprint.users.User` + .. _sec-plugins-hook-cli-commands: octoprint.cli.commands diff --git a/src/octoprint/server/util/__init__.py b/src/octoprint/server/util/__init__.py index 5d202d16..e62e1a8e 100644 --- a/src/octoprint/server/util/__init__.py +++ b/src/octoprint/server/util/__init__.py @@ -11,8 +11,10 @@ import octoprint.server from octoprint.users import ApiUser from octoprint.util import deprecated +from octoprint.plugin import plugin_manager import flask as _flask +import logging from . import flask from . import sockjs @@ -55,14 +57,22 @@ def loginFromApiKeyRequestHandler(): """ apikey = get_api_key(_flask.request) - - if apikey and apikey != octoprint.server.UI_API_KEY and not octoprint.server.appSessionManager.validate(apikey): - user = get_user_for_apikey(apikey) - if user is not None and _flask.ext.login.login_user(user, remember=False): - _flask.ext.principal.identity_changed.send(_flask.current_app._get_current_object(), - identity=_flask.ext.principal.Identity(user.get_id())) - else: - return _flask.make_response("Invalid API key", 401) + + if not apikey: + return + + if apikey == octoprint.server.UI_API_KEY: + return + + if octoprint.server.appSessionManager.validate(apikey): + return + + user = get_user_for_apikey(apikey) + if user is not None and _flask.ext.login.login_user(user, remember=False): + _flask.ext.principal.identity_changed.send(_flask.current_app._get_current_object(), + identity=_flask.ext.principal.Identity(user.get_id())) + else: + return _flask.make_response("Invalid API key", 401) def corsRequestHandler(): @@ -143,9 +153,21 @@ def get_user_for_apikey(apikey): if apikey == settings().get(["api", "key"]) or octoprint.server.appSessionManager.validate(apikey): # master key or an app session key was used return ApiUser() - elif octoprint.server.userManager.enabled: - # user key might have been used - return octoprint.server.userManager.findUser(apikey=apikey) + + if octoprint.server.userManager.enabled: + user = octoprint.server.userManager.findUser(apikey=apikey) + if user is not None: + # user key was used + return user + + apikey_hooks = plugin_manager().get_hooks("octoprint.accesscontrol.keyvalidator") + for name, hook in apikey_hooks.items(): + try: + user = hook(apikey) + if user is not None: + return user + except: + logging.getLogger(__name__).exception("Error running api key validator for plugin {} and key {}".format(name, apikey)) return None