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:
parent
9b457cce9e
commit
e8349d6593
4 changed files with 61 additions and 61 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue