From 3e5923b21e337db42106a2948ce439cf827b5703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 13 Mar 2017 16:17:25 +0100 Subject: [PATCH] Consolidate API Key handling System wide API key now offers a generate button like the user API keys. Setting the API key directly to a value via the settings API endpoint is now no longer possible, which should prevent setting it accidentally thanks to the browser prefilling things where it shouldn't. No delete button is offered for the system wide API key since it will get automatically regenerated on server start if not set, so regeneration is the only functionality here that makes sense. If no API key is set in the user settings, the "delete" button is now disabled. If a key is already set and a new one is to be generated, a confirmation dialog makes sure this is really what the user wants. Same for deleting an existing API key. Both the system wide API key and the user specific API keys will now only display a QRCode if there's actually a value for the key. --- docs/api/settings.rst | 20 +++++++++++++ docs/jsclientlib/settings.rst | 7 +++++ src/octoprint/server/api/settings.py | 22 +++++++++++++-- src/octoprint/settings.py | 14 ++++++++-- .../static/js/app/client/settings.js | 5 ++++ .../static/js/app/viewmodels/settings.js | 13 +++++++++ .../static/js/app/viewmodels/usersettings.js | 28 +++++++++++++++---- .../templates/dialogs/settings/api.jinja2 | 8 ++++-- .../dialogs/usersettings/access.jinja2 | 10 +++---- 9 files changed, 109 insertions(+), 18 deletions(-) diff --git a/docs/api/settings.rst b/docs/api/settings.rst index 345a4413..acc79722 100644 --- a/docs/api/settings.rst +++ b/docs/api/settings.rst @@ -68,6 +68,23 @@ Save settings // ... } +.. _sec-api-settings-generateapikey: + +Regenerate the system wide API key +================================== + +.. http:post:: /api/settings/apikey + + Generates a new system wide API key. + + Does not expect a body. Will return the generated API key as ``apikey`` + property in the JSON object contained in the response body. + + Requires admin rights. + + :status 200: No error + :status 403: No admin rights + .. _sec-api-settings-datamodel: Data model @@ -87,6 +104,9 @@ mapped from the same fields in ``config.yaml`` unless otherwise noted: - * - ``api.key`` - Only maps to ``api.key`` in ``config.yaml`` if request is sent with admin rights, set to ``n/a`` otherwise. + Starting with OctoPrint 1.3.3 setting this field via :ref:`the API ` is not possible, + only :ref:`regenerting it ` is supported. Setting a custom value is only + possible through `config.yaml`. * - ``api.allowCrossOrigin`` - * - ``appearance.name`` diff --git a/docs/jsclientlib/settings.rst b/docs/jsclientlib/settings.rst index ad804b8b..a8894718 100644 --- a/docs/jsclientlib/settings.rst +++ b/docs/jsclientlib/settings.rst @@ -35,6 +35,13 @@ :param object opts: Additional options for the request :returns Promise: A `jQuery Promise `_ for the request's response +.. js:function:: OctoPrintClient.settings.generateApiKey(opts) + + Generate a new system wide API key. + + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. seealso:: :ref:`Settings API ` diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 0276eb01..c03315a6 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -15,7 +15,7 @@ from octoprint.events import eventManager, Events from octoprint.settings import settings, valid_boolean_trues from octoprint.server import admin_permission, printer -from octoprint.server.api import api +from octoprint.server.api import api, NO_CONTENT from octoprint.server.util.flask import restricted_access, with_revalidation_checking import octoprint.plugin @@ -83,7 +83,7 @@ def getSettings(): data = { "api": { "enabled": s.getBoolean(["api", "enabled"]), - "key": s.get(["api", "key"]) if admin_permission.can() else "n/a", + "key": s.get(["api", "key"]) if admin_permission.can() else None, "allowCrossOrigin": s.get(["api", "allowCrossOrigin"]) }, "appearance": { @@ -258,6 +258,23 @@ def setSettings(): _saveSettings(data) return getSettings() + +@api.route("/settings/apikey", methods=["POST"]) +@restricted_access +@admin_permission.require(403) +def generateApiKey(): + apikey = settings().generateApiKey() + return jsonify(apikey=apikey) + + +@api.route("/settings/apikey", methods=["DELETE"]) +@restricted_access +@admin_permission.require(403) +def deleteApiKey(): + settings().deleteApiKey() + return NO_CONTENT + + def _saveSettings(data): logger = logging.getLogger(__name__) @@ -268,7 +285,6 @@ def _saveSettings(data): if "api" in data.keys(): if "enabled" in data["api"].keys(): s.setBoolean(["api", "enabled"], data["api"]["enabled"]) - if "key" in data["api"].keys(): s.set(["api", "key"], data["api"]["key"], True) if "allowCrossOrigin" in data["api"].keys(): s.setBoolean(["api", "allowCrossOrigin"], data["api"]["allowCrossOrigin"]) if "appearance" in data.keys(): diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 57c3f49d..7ea1e7a0 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -557,8 +557,7 @@ class Settings(object): self.load(migrate=True) if self.get(["api", "key"]) is None: - self.set(["api", "key"], ''.join('%02X' % z for z in bytes(uuid.uuid4().bytes))) - self.save(force=True) + self.generateApiKey() self._script_env = self._init_script_templating() @@ -1497,6 +1496,17 @@ class Settings(object): with atomic_write(filename, "wb", max_permissions=0o666) as f: f.write(script) + def generateApiKey(self): + apikey = ''.join('%02X' % z for z in bytes(uuid.uuid4().bytes)) + self.set(["api", "key"], apikey) + self.save(force=True) + return apikey + + def deleteApiKey(self): + self.set(["api", "key"], None) + self.save(force=True) + + def _default_basedir(applicationName): # taken from http://stackoverflow.com/questions/1084697/how-do-i-store-desktop-application-data-in-a-cross-platform-way-for-python if sys.platform == "darwin": diff --git a/src/octoprint/static/js/app/client/settings.js b/src/octoprint/static/js/app/client/settings.js index 81157bc0..b1bd6e62 100644 --- a/src/octoprint/static/js/app/client/settings.js +++ b/src/octoprint/static/js/app/client/settings.js @@ -6,6 +6,7 @@ } })(this, function(OctoPrintClient, $) { var url = "api/settings"; + var apiKeyUrl = url + "/apikey"; var OctoPrintSettingsClient = function(base) { this.base = base; @@ -40,6 +41,10 @@ return this.save(data, opts); }; + OctoPrintSettingsClient.prototype.generateApiKey = function (opts) { + return this.base.postJson(apiKeyUrl, opts); + }; + OctoPrintClient.registerComponent("settings", OctoPrintSettingsClient); return OctoPrintSettingsClient; }); diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 1125a2ff..785abf97 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -415,6 +415,19 @@ $(function() { self.settingsDialog.modal("hide"); }; + self.generateApiKey = function() { + if (!CONFIG_ACCESS_CONTROL) return; + + showConfirmationDialog(gettext("This will generate a new API Key. The old API Key will cease to function immediately."), + function() { + OctoPrint.settings.generateApiKey() + .done(function(response) { + self.api_key(response.apikey); + self.requestData(); + }); + }); + }; + self.showTranslationManager = function() { self.translationManagerDialog.modal(); return false; diff --git a/src/octoprint/static/js/app/viewmodels/usersettings.js b/src/octoprint/static/js/app/viewmodels/usersettings.js index b07e92f9..27bfbae9 100644 --- a/src/octoprint/static/js/app/viewmodels/usersettings.js +++ b/src/octoprint/static/js/app/viewmodels/usersettings.js @@ -71,16 +71,32 @@ $(function() { self.generateApikey = function() { if (!CONFIG_ACCESS_CONTROL) return; - self.users.generateApikey(self.currentUser().name, function(response) { - self.access_apikey(response.apikey); - }); + + var generate = function() { + self.users.generateApikey(self.currentUser().name) + .done(function(response) { + self.access_apikey(response.apikey); + }); + }; + + if (self.access_apikey()) { + showConfirmationDialog(gettext("This will generate a new API Key. The old API Key will cease to function immediately."), + generate); + } else { + generate(); + } }; self.deleteApikey = function() { if (!CONFIG_ACCESS_CONTROL) return; - self.users.deleteApikey(self.currentUser().name, function() { - self.access_apikey(undefined); - }); + if (!self.access_apikey()) return; + + showConfirmationDialog(gettext("This will delete the API Key. It will cease to to function immediately."), function() { + self.users.deleteApikey(self.currentUser().name) + .done(function() { + self.access_apikey(undefined); + }); + }) }; self.updateSettings = function(username, settings) { diff --git a/src/octoprint/templates/dialogs/settings/api.jinja2 b/src/octoprint/templates/dialogs/settings/api.jinja2 index a2c99b91..8237476a 100644 --- a/src/octoprint/templates/dialogs/settings/api.jinja2 +++ b/src/octoprint/templates/dialogs/settings/api.jinja2 @@ -16,10 +16,14 @@
- +
+ + +
+ {{ _('Please note that changes to the API key are applied immediately, without having to "Save" first.') }}
-
+
diff --git a/src/octoprint/templates/dialogs/usersettings/access.jinja2 b/src/octoprint/templates/dialogs/usersettings/access.jinja2 index 6cb7e2f9..67dee1f9 100644 --- a/src/octoprint/templates/dialogs/usersettings/access.jinja2 +++ b/src/octoprint/templates/dialogs/usersettings/access.jinja2 @@ -24,14 +24,14 @@
- - - + + +
- + {{ _('Please note that changes to the API key are applied immediately, without having to "Confirm" first.') }}
-
+