Also protect resources from blueprint plugins with the api key (unless the plugin specifies otherwise)
This commit is contained in:
parent
fec51ebc8e
commit
254145da22
4 changed files with 101 additions and 77 deletions
|
|
@ -176,6 +176,13 @@ class BlueprintPlugin(Plugin):
|
|||
|
||||
return None
|
||||
|
||||
def is_blueprint_protected(self):
|
||||
"""
|
||||
Whether the blueprint is supposed to be protected by API key (the default) or not.
|
||||
"""
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class SettingsPlugin(Plugin):
|
||||
def on_settings_load(self):
|
||||
|
|
|
|||
|
|
@ -306,9 +306,20 @@ class Server():
|
|||
app.register_blueprint(apps, url_prefix="/apps")
|
||||
|
||||
# also register any blueprints defined in BlueprintPlugins
|
||||
octoprint.plugin.call_plugin(octoprint.plugin.types.BlueprintPlugin,
|
||||
"get_blueprint",
|
||||
callback=lambda name, _, blueprint: app.register_blueprint(blueprint, url_prefix="/plugin/{name}".format(name=name)))
|
||||
blueprint_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.BlueprintPlugin)
|
||||
for name, plugin in blueprint_plugins.items():
|
||||
blueprint = plugin.get_blueprint()
|
||||
if blueprint is None:
|
||||
continue
|
||||
|
||||
if plugin.is_blueprint_protected():
|
||||
from octoprint.server.util import apiKeyRequestHandler, corsResponseHandler
|
||||
blueprint.before_request(apiKeyRequestHandler)
|
||||
blueprint.after_request(corsResponseHandler)
|
||||
|
||||
url_prefix = "/plugin/{name}".format(name=name)
|
||||
app.register_blueprint(blueprint, url_prefix=url_prefix)
|
||||
logger.debug("Registered API of plugin {name} under URL prefix {url_prefix}".format(name=name, url_prefix=url_prefix))
|
||||
|
||||
self._router = SockJSRouter(self._createSocketConnection, "/sockjs")
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ import octoprint.util as util
|
|||
import octoprint.users
|
||||
import octoprint.server
|
||||
import octoprint.plugin
|
||||
from octoprint.server import admin_permission, NO_CONTENT, UI_API_KEY, appSessionManager
|
||||
from octoprint.server import admin_permission, NO_CONTENT
|
||||
from octoprint.settings import settings as s, valid_boolean_trues
|
||||
from octoprint.server.util import get_api_key, get_user_for_apikey
|
||||
from octoprint.server.util import apiKeyRequestHandler, corsResponseHandler
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
|
||||
|
||||
|
|
@ -40,78 +40,8 @@ from . import slicing as api_slicing
|
|||
|
||||
VERSION = "0.1"
|
||||
|
||||
|
||||
def optionsAllowOrigin(request):
|
||||
""" Always reply 200 on OPTIONS request """
|
||||
|
||||
resp = current_app.make_default_options_response()
|
||||
|
||||
# Allow the origin which made the XHR
|
||||
resp.headers['Access-Control-Allow-Origin'] = request.headers['Origin']
|
||||
# Allow the actual method
|
||||
resp.headers['Access-Control-Allow-Methods'] = request.headers['Access-Control-Request-Method']
|
||||
# Allow for 10 seconds
|
||||
resp.headers['Access-Control-Max-Age'] = "10"
|
||||
|
||||
# 'preflight' request contains the non-standard headers the real request will have (like X-Api-Key)
|
||||
customRequestHeaders = request.headers.get('Access-Control-Request-Headers', None)
|
||||
if customRequestHeaders is not None:
|
||||
# If present => allow them all
|
||||
resp.headers['Access-Control-Allow-Headers'] = customRequestHeaders
|
||||
|
||||
return resp
|
||||
|
||||
@api.before_request
|
||||
def beforeApiRequests():
|
||||
"""
|
||||
All requests in this blueprint 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 or a user specific case. In any
|
||||
case it has to be present and must be valid, so anything other than the above three types will result in denying
|
||||
the request.
|
||||
"""
|
||||
|
||||
if request.method == 'OPTIONS' and s().getBoolean(["api", "allowCrossOrigin"]):
|
||||
return optionsAllowOrigin(request)
|
||||
|
||||
apikey = get_api_key(request)
|
||||
if apikey is None:
|
||||
# no api key => 401
|
||||
return make_response("No API key provided", 401)
|
||||
|
||||
if apikey == UI_API_KEY:
|
||||
# ui api key => continue regular request processing
|
||||
return
|
||||
|
||||
if not s().get(["api", "enabled"]):
|
||||
# api disabled => 401
|
||||
return make_response("API disabled", 401)
|
||||
|
||||
if apikey == s().get(["api", "key"]):
|
||||
# global api key => continue regular request processing
|
||||
return
|
||||
|
||||
if 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
|
||||
|
||||
# invalid api key => 401
|
||||
return make_response("Invalid API key", 401)
|
||||
|
||||
@api.after_request
|
||||
def afterApiRequests(resp):
|
||||
|
||||
# Allow crossdomain
|
||||
allowCrossOrigin = s().getBoolean(["api", "allowCrossOrigin"])
|
||||
if request.method != 'OPTIONS' and 'Origin' in request.headers and allowCrossOrigin:
|
||||
resp.headers['Access-Control-Allow-Origin'] = request.headers['Origin']
|
||||
|
||||
return resp
|
||||
|
||||
api.before_request(apiKeyRequestHandler)
|
||||
api.after_request(corsResponseHandler)
|
||||
|
||||
#~~ data from plugins
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,88 @@ import octoprint.timelapse
|
|||
import octoprint.server
|
||||
from octoprint.users import ApiUser
|
||||
|
||||
import flask as _flask
|
||||
|
||||
from . import flask
|
||||
from . import sockjs
|
||||
from . import tornado
|
||||
from . import watchdog
|
||||
|
||||
|
||||
def apiKeyRequestHandler():
|
||||
"""
|
||||
All requests in this blueprint 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 or a user specific case. In any
|
||||
case it has to be present and must be valid, so anything other than the above three types will result in denying
|
||||
the request.
|
||||
"""
|
||||
|
||||
import octoprint.server
|
||||
|
||||
if _flask.request.method == 'OPTIONS' and settings().getBoolean(["api", "allowCrossOrigin"]):
|
||||
return optionsAllowOrigin(_flask.request)
|
||||
|
||||
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().get(["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
|
||||
|
||||
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
|
||||
|
||||
# invalid api key => 401
|
||||
return _flask.make_response("Invalid API key", 401)
|
||||
|
||||
|
||||
def corsResponseHandler(resp):
|
||||
|
||||
# Allow crossdomain
|
||||
allowCrossOrigin = settings().getBoolean(["api", "allowCrossOrigin"])
|
||||
if _flask.request.method != 'OPTIONS' and 'Origin' in _flask.request.headers and allowCrossOrigin:
|
||||
resp.headers['Access-Control-Allow-Origin'] = _flask.request.headers['Origin']
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
def optionsAllowOrigin(request):
|
||||
""" Always reply 200 on OPTIONS request """
|
||||
|
||||
resp = _flask.current_app.make_default_options_response()
|
||||
|
||||
# Allow the origin which made the XHR
|
||||
resp.headers['Access-Control-Allow-Origin'] = request.headers['Origin']
|
||||
# Allow the actual method
|
||||
resp.headers['Access-Control-Allow-Methods'] = request.headers['Access-Control-Request-Method']
|
||||
# Allow for 10 seconds
|
||||
resp.headers['Access-Control-Max-Age'] = "10"
|
||||
|
||||
# 'preflight' request contains the non-standard headers the real request will have (like X-Api-Key)
|
||||
customRequestHeaders = request.headers.get('Access-Control-Request-Headers', None)
|
||||
if customRequestHeaders is not None:
|
||||
# If present => allow them all
|
||||
resp.headers['Access-Control-Allow-Headers'] = customRequestHeaders
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
def get_user_for_apikey(apikey):
|
||||
if settings().get(["api", "enabled"]) and apikey is not None:
|
||||
if apikey == settings().get(["api", "key"]) or octoprint.server.appSessionManager.validate(apikey):
|
||||
|
|
|
|||
Loading…
Reference in a new issue