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:
parent
c1fdbaa1e7
commit
3e5923b21e
9 changed files with 109 additions and 18 deletions
|
|
@ -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``
|
||||
|
|
|
|||
|
|
@ -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>`
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue