From e8349d65935f4b81da81796e23db83bf15bdd6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 15 Feb 2017 13:12:35 +0100 Subject: [PATCH] Always create a user session for requests with an API key API endpoints that were not decorated with restricted_access so far did not properly create a user session for cookie-less requests with an API key only. That meant that flask.current_user stayed an anonymous user, even with the global admin API key. In case of /api/settings that meant that even with the global admin API key it was not possible to read settings that are admin-only (like the API key for example) or user-only. This has now been rectified by moving the session creation into a different before_request handler that is registered globally on all API endpoints, meaning that an API key will now always lead to a user session to be created, regardless of the requirements of the API endpoint in question. Additionally the CORS handling was extracted as well as the API key presence enforcement. BlueprintPlugins will now also get the CORS and session-from-API-Key treatment if they do not declare their endpoints as restricted. Might solve the API key "n/a" issue filed in #1751, but since the cause of that isn't identified yet that's not sure. --- src/octoprint/server/__init__.py | 24 +++++++--- src/octoprint/server/api/__init__.py | 6 ++- src/octoprint/server/util/__init__.py | 63 +++++++++++++++------------ src/octoprint/server/util/flask.py | 29 ++---------- 4 files changed, 61 insertions(+), 61 deletions(-) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 6d2c9a9d..f1d48477 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -66,6 +66,8 @@ import octoprint.util import octoprint.filemanager.storage import octoprint.filemanager.analysis import octoprint.slicing +from octoprint.server.util import enforceApiKeyRequestHandler, loginFromApiKeyRequestHandler, corsRequestHandler, \ + corsResponseHandler from octoprint.server.util.flask import PreemptiveCache from . import util @@ -365,9 +367,14 @@ class Server(object): as_attachment=True, allow_client_caching=False ) + additional_mime_types=dict(mime_type_guesser=mime_type_guesser) + admin_validator = dict(access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator)) - no_hidden_files_validator = dict(path_validation=util.tornado.path_validation_factory(lambda path: not octoprint.util.is_hidden_path(path), status_code=404)) + user_validator = dict(access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator)) + + no_hidden_files_validator = dict(path_validation=util.tornado.path_validation_factory(lambda path: not octoprint.util.is_hidden_path(path), + status_code=404)) def joined_dict(*dicts): if not len(dicts): @@ -392,9 +399,9 @@ class Server(object): download_handler_kwargs, admin_validator)), # camera snapshot - (r"/downloads/camera/current", util.tornado.UrlProxyHandler, dict(url=self._settings.get(["webcam", "snapshot"]), - as_attachment=True, - access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))), + (r"/downloads/camera/current", util.tornado.UrlProxyHandler, joined_dict(dict(url=self._settings.get(["webcam", "snapshot"]), + as_attachment=True), + user_validator)), # generated webassets (r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=os.path.join(self._settings.getBaseFolder("generated"), "webassets"))), @@ -894,8 +901,13 @@ class Server(object): return if plugin.is_blueprint_protected(): - from octoprint.server.util import apiKeyRequestHandler, corsResponseHandler - blueprint.before_request(apiKeyRequestHandler) + blueprint.before_request(corsRequestHandler) + blueprint.before_request(enforceApiKeyRequestHandler) + blueprint.before_request(loginFromApiKeyRequestHandler) + blueprint.after_request(corsResponseHandler) + else: + blueprint.before_request(corsRequestHandler) + blueprint.before_request(loginFromApiKeyRequestHandler) blueprint.after_request(corsResponseHandler) url_prefix = "/plugin/{name}".format(name=name) diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 473560b0..35401e45 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -19,7 +19,7 @@ import octoprint.server import octoprint.plugin from octoprint.server import admin_permission, NO_CONTENT from octoprint.settings import settings as s, valid_boolean_trues -from octoprint.server.util import noCachingExceptGetResponseHandler, apiKeyRequestHandler, corsResponseHandler +from octoprint.server.util import noCachingExceptGetResponseHandler, enforceApiKeyRequestHandler, loginFromApiKeyRequestHandler, corsRequestHandler, corsResponseHandler from octoprint.server.util.flask import restricted_access, get_json_command_from_request, passive_login @@ -45,7 +45,9 @@ VERSION = "0.1" api.after_request(noCachingExceptGetResponseHandler) -api.before_request(apiKeyRequestHandler) +api.before_request(corsRequestHandler) +api.before_request(enforceApiKeyRequestHandler) +api.before_request(loginFromApiKeyRequestHandler) api.after_request(corsResponseHandler) #~~ data from plugins diff --git a/src/octoprint/server/util/__init__.py b/src/octoprint/server/util/__init__.py index dad72cf8..5d202d16 100644 --- a/src/octoprint/server/util/__init__.py +++ b/src/octoprint/server/util/__init__.py @@ -10,6 +10,8 @@ import octoprint.timelapse import octoprint.server from octoprint.users import ApiUser +from octoprint.util import deprecated + import flask as _flask from . import flask @@ -18,51 +20,58 @@ from . import tornado from . import watchdog -def apiKeyRequestHandler(): +def enforceApiKeyRequestHandler(): """ - ``before_request`` handler for blueprints for which all requests need to be made supplying an API key. - - This may be the UI_API_KEY, in which case the underlying request processing will directly take place, or it may be - the global, an app specific or a user specific one. In any case it has to be present and must be valid, so anything - other than the above types will result in the application denying the request. + ``before_request`` handler for blueprints which makes sure an API key is provided """ import octoprint.server - if _flask.request.method == 'OPTIONS' and settings().getBoolean(["api", "allowCrossOrigin"]): - return optionsAllowOrigin(_flask.request) + if _flask.request.method == 'OPTIONS': + # we ignore OPTIONS requests here + return - if _flask.request.endpoint == "static" or _flask.request.endpoint.endswith(".static"): + if _flask.request.endpoint and (_flask.request.endpoint == "static" or _flask.request.endpoint.endswith(".static")): + # no further handling for static resources return apikey = get_api_key(_flask.request) + if apikey is None: - # no api key => 401 return _flask.make_response("No API key provided", 401) - if apikey == octoprint.server.UI_API_KEY: - # ui api key => continue regular request processing - return - - if not settings().getBoolean(["api", "enabled"]): + if apikey != octoprint.server.UI_API_KEY and not settings().getBoolean(["api", "enabled"]): # api disabled => 401 return _flask.make_response("API disabled", 401) - if apikey == settings().get(["api", "key"]): - # global api key => continue regular request processing - return +apiKeyRequestHandler = deprecated("apiKeyRequestHandler has been renamed to enforceApiKeyRequestHandler")(enforceApiKeyRequestHandler) - if octoprint.server.appSessionManager.validate(apikey): - # app session key => continue regular request processing - return - user = get_user_for_apikey(apikey) - if user is not None: - # user specific api key => continue regular request processing - return +def loginFromApiKeyRequestHandler(): + """ + ``before_request`` handler for blueprints which creates a login session for the provided api key (if available) - # invalid api key => 401 - return _flask.make_response("Invalid API key", 401) + UI_API_KEY and app session keys are handled as anonymous keys here and ignored. + """ + + 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) + + +def corsRequestHandler(): + """ + ``before_request`` handler for blueprints which sets CORS headers for OPTIONS requests if enabled + """ + if _flask.request.method == 'OPTIONS' and settings().getBoolean(["api", "allowCrossOrigin"]): + # reply to OPTIONS request for CORS headers + return optionsAllowOrigin(_flask.request) def corsResponseHandler(resp): diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index 1983e9d9..5f41d661 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -1089,20 +1089,13 @@ def restricted_access(func): """ If you decorate a view with this, it will ensure that first setup has been done for OctoPrint's Access Control plus that any conditions of the - login_required decorator are met. It also allows to login using the masterkey or any - of the user's apikeys if API access is enabled globally and for the decorated view. + login_required decorator are met (possibly through a session already created + by octoprint.server.util.apiKeyRequestHandler earlier in the request processing). If OctoPrint's Access Control has not been setup yet (indicated by the "firstRun" flag from the settings being set to True and the userManager not indicating that it's user database has been customized from default), the decorator will cause a HTTP 403 status code to be returned by the decorated resource. - - If the API key matches the UI API key, the result of calling login_required for the - view will be returned (browser session mode). - - Otherwise the API key will be attempted to be resolved to a user. If that is - successful the user will be logged in and the view will be called directly. - Otherwise a HTTP 401 status code will be returned. """ @functools.wraps(func) def decorated_view(*args, **kwargs): @@ -1110,23 +1103,7 @@ def restricted_access(func): if settings().getBoolean(["server", "firstRun"]) and settings().getBoolean(["accessControl", "enabled"]) and (octoprint.server.userManager is None or not octoprint.server.userManager.hasBeenCustomized()): return flask.make_response("OctoPrint isn't setup yet", 403) - apikey = octoprint.server.util.get_api_key(flask.request) - if apikey == octoprint.server.UI_API_KEY: - # UI API key => call regular login_required decorator, we are using browser sessions here - return flask.ext.login.login_required(func)(*args, **kwargs) - - # try to determine user for key - user = octoprint.server.util.get_user_for_apikey(apikey) - if user is None: - # no user or no key => go away - return flask.make_response("Invalid API key", 401) - - if not flask.ext.login.login_user(user, remember=False): - # user for API key could not be logged in => go away - return flask.make_response("Invalid API key", 401) - - flask.ext.principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask.ext.principal.Identity(user.get_id())) - return func(*args, **kwargs) + return flask.ext.login.login_required(func)(*args, **kwargs) return decorated_view