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