From ace102e1c18499f409380b23973387dfa570f986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 24 Feb 2014 09:57:51 +0100 Subject: [PATCH] Added API documentation, adjusted API and returned data model slightly to be consistent with other parts of the API, made log file management restricted to admins. TODO: Make downloading of logs also restricted to admins --- docs/api/index.rst | 2 +- docs/api/logs.rst | 177 ++++++++++++++++++++++++ src/octoprint/server/__init__.py | 2 +- src/octoprint/server/api/files.py | 8 +- src/octoprint/server/api/log.py | 34 +++-- src/octoprint/server/util.py | 55 +++++++- src/octoprint/templates/settings.jinja2 | 2 +- 7 files changed, 253 insertions(+), 27 deletions(-) create mode 100644 docs/api/logs.rst diff --git a/docs/api/index.rst b/docs/api/index.rst index 8d10a202..7d296a1b 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -12,4 +12,4 @@ API Documentation connection.rst printer.rst job.rst - + logs.rst diff --git a/docs/api/logs.rst b/docs/api/logs.rst new file mode 100644 index 00000000..abdb9ceb --- /dev/null +++ b/docs/api/logs.rst @@ -0,0 +1,177 @@ +.. _sec-api-logs: + +******************* +Log file management +******************* + +.. note:: + + All log file management operations require admin rights. + +.. contents:: + +.. _sec-api-logs-list: + +Retrieve a list of available log files +====================================== + +.. http:post:: /api/logs + + Retrieve information regarding all log files currently available and regarding the disk space still available + in the system on the location the log files are being stored. + + Returns a :ref:`Logfile Retrieve response `. + + **Example Request** + + .. sourcecode:: http + + GET /api/logs HTTP/1.1 + Host: example.com + X-Api-Key: abcdef... + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "files" : [ + { + "date" : 1393158814, + "name" : "octoprint.log", + "size" : 43712, + "refs": { + "resource": "http://example.com/api/logs/octoprint.log", + "download": "http://example.com/downloads/logs/octoprint.log" + } + }, + { + "date" : 1392628936, + "name" : "octoprint.log.2014-02-17", + "size" : 13205, + "refs": { + "resource": "http://example.com/api/logs/octoprint.log.2014-02-17", + "download": "http://example.com/downloads/logs/octoprint.log.2014-02-17" + } + }, + { + "date" : 1393158814, + "name" : "serial.log", + "size" : 1798419, + "refs": { + "resource": "http://example.com/api/logs/serial.log", + "download": "http://example.com/downloads/logs/serial.log" + } + } + ], + "free": 12237201408 + } + + :statuscode 200: No error + :statuscode 403: If the given API token did not have admin rights associated with it + +.. _sec-api-logs-delete: + +Delete a specific logfile +========================= + +.. http:delete:: /api/logs/(path:filename) + + Delete the selected log file with name `filename`. + + Returns a :http:statuscode:`204` after successful deletion. + + **Example Request** + + .. sourcecode:: http + + DELETE /api/logs/octoprint.log.2014-02-17 HTTP/1.1 + Host: example.com + X-Api-Key: abcdef... + + :param filename: The filename of the log file to delete + :statuscode 204: No error + :statuscode 403: If the given API token did not have admin rights associated with it + :statuscode 404: If the file was not found + +.. _sec-api-logs-datamodel: + +Datamodel +========= + +.. _sec-api-logs-datamodel-retrieveresponse: + +Logfile Retrieve Response +------------------------- + +.. list-table:: + :widths: 15 5 10 30 + :header-rows: 1 + + * - Name + - Multiplicity + - Type + - Description + * - ``files`` + - 0..* + - Array of :ref:`File information items ` + - The list of requested files. Might be an empty list if no files are available + * - ``free`` + - 1 + - String + - The amount of disk space in bytes available in the local disk space (refers to OctoPrint's ``logs`` folder). + +.. _sec-api-logs-datamodel-fileinfo: + +File information +---------------- + +.. list-table:: + :widths: 15 5 10 30 + :header-rows: 1 + + * - Name + - Multiplicity + - Type + - Description + * - ``name`` + - 1 + - String + - The name of the file + * - ``size`` + - 1 + - Number + - The size of the file in bytes. + * - ``date`` + - 1 + - Unix timestamp + - The timestamp when this file was last modified. + * - ``refs`` + - 1 + - :ref:`sec-api-logs-datamodel-ref` + - References relevant to this file + +.. _sec-api-logs-datamodel-ref: + +References +---------- + +.. list-table:: + :widths: 15 5 10 30 + :header-rows: 1 + + * - Name + - Multiplicity + - Type + - Description + * - ``resource`` + - 1 + - URL + - The resource that represents the file (e.g. for deleting) + * - ``download`` + - 1 + - URL + - The download URL for the file diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index aa6af8de..79689adc 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -28,7 +28,7 @@ admin_permission = Permission(RoleNeed("admin")) user_permission = Permission(RoleNeed("user")) -from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection +from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection, admin_validator from octoprint.printer import Printer, getConnectionOptions from octoprint.settings import settings import octoprint.gcodefiles as gcodefiles diff --git a/src/octoprint/server/api/files.py b/src/octoprint/server/api/files.py index c704faf2..bba7b260 100644 --- a/src/octoprint/server/api/files.py +++ b/src/octoprint/server/api/files.py @@ -11,7 +11,6 @@ import octoprint.util as util from octoprint.filemanager.destinations import FileDestinations from octoprint.settings import settings, valid_boolean_trues from octoprint.server import printer, gcodeManager, eventManager, restricted_access, NO_CONTENT -from octoprint.server.util import urlForDownload from octoprint.server.api import api @@ -66,7 +65,7 @@ def _getFileList(origin): file.update({ "refs": { "resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=file["name"], _external=True), - "download": urlForDownload(FileDestinations.LOCAL, file["name"]) + "download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + file["name"] } }) return files @@ -169,7 +168,7 @@ def uploadGcodeFile(target): "origin": FileDestinations.LOCAL, "refs": { "resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=filename, _external=True), - "download": urlForDownload(FileDestinations.LOCAL, filename) + "download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + filename } } }) @@ -269,6 +268,5 @@ def deleteGcodeFile(filename, target): else: gcodeManager.removeFile(filename) - # return an updated list of files - return readGcodeFiles() + return NO_CONTENT diff --git a/src/octoprint/server/api/log.py b/src/octoprint/server/api/log.py index c2699628..44159d27 100644 --- a/src/octoprint/server/api/log.py +++ b/src/octoprint/server/api/log.py @@ -4,35 +4,43 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp import os -from flask import request, jsonify, make_response, url_for +from flask import request, jsonify, url_for, make_response from werkzeug.utils import secure_filename from octoprint.settings import settings -from octoprint.server import restricted_access +from octoprint.server import restricted_access, NO_CONTENT, admin_permission from octoprint.server.util import redirectToTornado from octoprint.server.api import api +from octoprint.util import getFreeBytes @api.route("/logs", methods=["GET"]) -def getLogData(): +@restricted_access +@admin_permission.require(403) +def getLogFiles(): files = _getLogFiles() - return jsonify(files=files) + return jsonify(files=files, free=getFreeBytes(settings().getBaseFolder("logs"))) -@api.route("/logs/", methods=["GET"]) +@api.route("/logs/", methods=["GET"]) +@restricted_access +@admin_permission.require(403) def downloadLog(filename): return redirectToTornado(request, url_for("index") + "downloads/logs/" + filename) -@api.route("/logs/", methods=["DELETE"]) +@api.route("/logs/", methods=["DELETE"]) @restricted_access +@admin_permission.require(403) def deleteLog(filename): secure = os.path.join(settings().getBaseFolder("logs"), secure_filename(filename)) - if os.path.exists(secure): - os.remove(secure) + if not os.path.exists(secure): + return make_response("File not found: %s" % filename, 404) - return getLogData() + os.remove(secure) + + return NO_CONTENT def _getLogFiles(): @@ -42,10 +50,12 @@ def _getLogFiles(): statResult = os.stat(os.path.join(basedir, osFile)) files.append({ "name": osFile, - "size": statResult.st_size, - "bytes": statResult.st_size, "date": int(statResult.st_mtime), - "url": url_for("index") + "downloads/logs/" + osFile + "size": statResult.st_size, + "refs": { + "resource": url_for(".downloadLog", filename=osFile, _external=True), + "download": url_for("index", _external=True) + "downloads/logs/" + osFile + } }) return files diff --git a/src/octoprint/server/util.py b/src/octoprint/server/util.py index 3ff9389d..2741441e 100644 --- a/src/octoprint/server/util.py +++ b/src/octoprint/server/util.py @@ -1,7 +1,11 @@ +# coding=utf-8 +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' + from flask.ext.principal import identity_changed, Identity from tornado.web import StaticFileHandler, HTTPError from flask import url_for, make_response, request, current_app -from flask.ext.login import login_required, login_user +from flask.ext.login import login_required, login_user, current_user from werkzeug.utils import redirect from sockjs.tornado import SockJSConnection @@ -21,6 +25,7 @@ import octoprint.server from octoprint.users import ApiUser from octoprint.events import Events + def restricted_access(func, apiEnabled=True): """ If you decorate a view with this, it will ensure that first setup has been @@ -80,11 +85,32 @@ def api_access(func): return decorated_view +def _getUserForApiKey(apikey): + if settings().get(["api", "enabled"]) and apikey is not None: + if apikey == settings().get(["api", "key"]): + # master key was used + return ApiUser() + else: + # user key might have been used + return octoprint.server.userManager.findUser(apikey=apikey) + else: + return None + + def _getApiKey(request): - if "apikey" in request.values: + # Check Flask GET/POST arguments + if hasattr(request, "values") and "apikey" in request.values: return request.values["apikey"] + + # Check Tornado GET/POST arguments + if hasattr(request, "arguments") and "apikey" in request.arguments \ + and len(request.arguments["apikey"]) > 0 and len(request.arguments["apikey"].strip()) > 0: + return request.arguments["apikey"] + + # Check Tornado and Flask headers if "X-Api-Key" in request.headers.keys(): return request.headers.get("X-Api-Key") + return None @@ -204,11 +230,15 @@ class LargeResponseHandler(StaticFileHandler): CHUNK_SIZE = 16 * 1024 - def initialize(self, path, default_filename=None, as_attachment=False): + def initialize(self, path, default_filename=None, as_attachment=False, access_validation=None): StaticFileHandler.initialize(self, path, default_filename) self._as_attachment = as_attachment + self._access_validation = access_validation def get(self, path, include_body=True): + if self._access_validation is not None: + self._access_validation(self.request) + path = self.parse_url_path(path) abspath = os.path.abspath(os.path.join(self.root, path)) # os.path.abspath strips a trailing / @@ -273,6 +303,21 @@ class LargeResponseHandler(StaticFileHandler): self.set_header("Content-Disposition", "attachment") +#~~ admin access validator for tornado + + +# TODO doesnt work for flask right now (no app context of course), try to figure out something +def admin_validator(request): + apikey = _getApiKey(request) + if settings().get(["api", "enabled"]) and apikey is not None: + user = _getUserForApiKey(apikey) + else: + user = current_user + + if user is None or not user.is_authenticated() or not user.is_admin(): + raise HTTPError(403) + + #~~ reverse proxy compatible wsgi middleware @@ -333,7 +378,3 @@ def redirectToTornado(request, target): redirectUrl += fragment return redirect(redirectUrl) - -def urlForDownload(origin, filename): - return url_for("index", _external=True) + "downloads/files/" + origin + "/" + filename - diff --git a/src/octoprint/templates/settings.jinja2 b/src/octoprint/templates/settings.jinja2 index 6b373253..802f1ac8 100644 --- a/src/octoprint/templates/settings.jinja2 +++ b/src/octoprint/templates/settings.jinja2 @@ -560,7 +560,7 @@ -  |  +  |