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.
This commit is contained in:
Gina Häußge 2017-02-15 13:12:35 +01:00
parent 9b457cce9e
commit e8349d6593
4 changed files with 61 additions and 61 deletions

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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