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.
This commit is contained in:
Gina Häußge 2017-03-13 16:17:25 +01:00
parent c1fdbaa1e7
commit 3e5923b21e
9 changed files with 109 additions and 18 deletions

View file

@ -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 <sec-api-settings-save>` is not possible,
only :ref:`regenerting it <sec-api-settings-generateapikey>` is supported. Setting a custom value is only
possible through `config.yaml`.
* - ``api.allowCrossOrigin``
-
* - ``appearance.name``

View file

@ -35,6 +35,13 @@
:param object opts: Additional options for the request
:returns Promise: A `jQuery Promise <http://api.jquery.com/Types/#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 <http://api.jquery.com/Types/#Promise>`_ for the request's response
.. seealso::
:ref:`Settings API <sec-api-settings>`

View file

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

View file

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

View file

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

View file

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

View file

@ -71,16 +71,32 @@ $(function() {
self.generateApikey = function() {
if (!CONFIG_ACCESS_CONTROL) return;
self.users.generateApikey(self.currentUser().name, function(response) {
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() {
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) {

View file

@ -16,10 +16,14 @@
<div class="control-group">
<label class="control-label" for="settings-apiKey">{{ _('API Key') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: api_key, valueUpdate: 'afterkeydown'" id="settings-apikey">
<div class="input-append">
<input type="text" class="input-block-level" disabled="disabled" id="settings-apikey" data-bind="value: api_key, attr: {placeholder: '{{ _('N/A') }}'}">
<button class="btn" title="Generate new API Key" data-bind="click: generateApiKey, enable: api_key"><i class="icon-refresh"></i></button>
</div>
<span class="help-block">{{ _('Please note that changes to the API key are applied immediately, without having to "Save" first.') }}</span>
</div>
</div>
<div class="control-group">
<div class="control-group" data-bind="visible: api_key">
<label class="control-label">{{ _('QR Code') }}</label>
<div class="controls">
<div data-bind="qrcode: {text: api_key, size: 180}"></div>

View file

@ -24,14 +24,14 @@
<label class="control-label" for="userSettings-access_apikey">{{ _('Current API Key') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level uneditable-input" id="userSettings-access_apikey" data-bind="value: access_apikey, valueUpdate: 'input', attr: {placeholder: '{{ _('N/A') }}'}">
<a class="btn" title="Generate new Apikey" data-bind="click: generateApikey"><i class="icon-refresh"></i></a>
<a class="btn btn-danger" title="Delete Apikey" data-bind="click: deleteApikey"><i class="icon-trash"></i></a>
<input type="text" class="input-block-level" disabled="disabled" id="userSettings-access_apikey" data-bind="value: access_apikey, attr: {placeholder: '{{ _('N/A') }}'}">
<button class="btn" title="Generate new API Key" data-bind="click: generateApikey"><i class="icon-refresh"></i></button>
<button class="btn btn-danger" title="Delete API Key" data-bind="click: deleteApikey, enable: access_apikey"><i class="icon-trash"></i></button>
</div>
<span class="help-block">{{ _('Please note that changes to the API key are applied immediately, without having to "Confirm" first.') }}</span>
</div>
</div>
<div class="control-group">
<div class="control-group" data-bind="visible: access_apikey">
<label class="control-label">{{ _('QR Code') }}</label>
<div class="controls">
<div data-bind="qrcode: {text: access_apikey, size: 150}"></div>