From 1febcd671ab7e81e1243c065053589b8a53e6da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 19 Mar 2013 23:06:48 +0100 Subject: [PATCH] "Writing" backend methods now protected (need logged in user (or dummy user if access control is disabled) to perform action), most "writing" frontend controls disabled if no logged in (or dummy) user returned from backend. --- octoprint/server.py | 25 +++- octoprint/static/js/ui.js | 218 +++++++++++++++++++-------------- octoprint/templates/index.html | 112 ++++++++--------- octoprint/users.py | 32 +++-- 4 files changed, 224 insertions(+), 163 deletions(-) diff --git a/octoprint/server.py b/octoprint/server.py index d59226e..c900f1f 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -138,11 +138,13 @@ def connect(): return jsonify(state="Connecting") @app.route(BASEURL + "control/disconnect", methods=["POST"]) +@login_required def disconnect(): printer.disconnect() return jsonify(state="Offline") @app.route(BASEURL + "control/command", methods=["POST"]) +@login_required def printerCommand(): if "application/json" in request.headers["Content-Type"]: data = request.json @@ -166,21 +168,25 @@ def printerCommand(): return jsonify(SUCCESS) @app.route(BASEURL + "control/print", methods=["POST"]) +@login_required def printGcode(): printer.startPrint() return jsonify(SUCCESS) @app.route(BASEURL + "control/pause", methods=["POST"]) +@login_required def pausePrint(): printer.togglePausePrint() return jsonify(SUCCESS) @app.route(BASEURL + "control/cancel", methods=["POST"]) +@login_required def cancelPrint(): printer.cancelPrint() return jsonify(SUCCESS) @app.route(BASEURL + "control/temperature", methods=["POST"]) +@login_required def setTargetTemperature(): if not printer.isOperational(): return jsonify(SUCCESS) @@ -198,6 +204,7 @@ def setTargetTemperature(): return jsonify(SUCCESS) @app.route(BASEURL + "control/jog", methods=["POST"]) +@login_required def jog(): if not printer.isOperational() or printer.isPrinting(): # do not jog when a print job is running or we don't have a connection @@ -234,6 +241,7 @@ def getSpeedValues(): return jsonify(feedrate=printer.feedrateState()) @app.route(BASEURL + "control/speed", methods=["POST"]) +@login_required def speed(): if not printer.isOperational(): return jsonify(SUCCESS) @@ -261,6 +269,7 @@ def readGcodeFile(filename): return send_from_directory(settings().getBaseFolder("uploads"), filename, as_attachment=True) @app.route(BASEURL + "gcodefiles/upload", methods=["POST"]) +@login_required def uploadGcodeFile(): filename = None if "gcode_file" in request.files.keys(): @@ -269,6 +278,7 @@ def uploadGcodeFile(): return jsonify(files=gcodeManager.getAllFileData(), filename=filename) @app.route(BASEURL + "gcodefiles/load", methods=["POST"]) +@login_required def loadGcodeFile(): if "filename" in request.values.keys(): printAfterLoading = False @@ -280,6 +290,7 @@ def loadGcodeFile(): return jsonify(SUCCESS) @app.route(BASEURL + "gcodefiles/delete", methods=["POST"]) +@login_required def deleteGcodeFile(): if "filename" in request.values.keys(): filename = request.values["filename"] @@ -318,6 +329,7 @@ def downloadTimelapse(filename): return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True) @app.route(BASEURL + "timelapse/", methods=["DELETE"]) +@login_required def deleteTimelapse(filename): if util.isAllowedFile(filename, set(["mpg"])): secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename)) @@ -326,6 +338,7 @@ def deleteTimelapse(filename): return getTimelapseData() @app.route(BASEURL + "timelapse/config", methods=["POST"]) +@login_required def setTimelapseConfig(): if request.values.has_key("type"): type = request.values["type"] @@ -389,6 +402,7 @@ def getSettings(): }) @app.route(BASEURL + "settings", methods=["POST"]) +@login_required def setSettings(): if "application/json" in request.headers["Content-Type"]: data = request.json @@ -434,6 +448,7 @@ def setSettings(): #~~ system control @app.route(BASEURL + "system", methods=["POST"]) +@login_required def performSystemAction(): logger = logging.getLogger(__name__) if request.values.has_key("action"): @@ -467,15 +482,14 @@ def login(): user = userManager.findUser(username) if user is not None: - passwordHash = users.UserManager.createPasswordHash(password) - if passwordHash == user.passwordHash: + if user.check_password(users.UserManager.createPasswordHash(password)): login_user(user, remember=remember) - return jsonify({"name": user.username, "roles": user.roles}) + return jsonify({"name": user.get_name(), "user": user.is_user(), "admin": user.is_admin()}) return app.make_response(("User unknown or password incorrect", 401, [])) elif "passive" in request.values.keys(): user = current_user if user is not None and not user.is_anonymous(): - return jsonify({"name": user.username, "roles": user.roles}) + return jsonify({"name": user.get_name(), "user": user.is_user(), "admin": user.is_admin()}) else: return jsonify(SUCCESS) @@ -488,8 +502,7 @@ def logout(): def load_user(id): if userManager is not None: return userManager.findUser(id) - else: - return users.DummyUser() + return None #~~ startup code class Server(): diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index 9cf01a3..6bef7b8 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -1,8 +1,80 @@ //~~ View models -function ConnectionViewModel() { +function LoginStateViewModel() { var self = this; + self.loggedIn = ko.observable(false); + self.username = ko.observable(undefined); + self.isAdmin = ko.observable(false); + self.isUser = ko.observable(false); + + self.userMenuText = ko.computed(function() { + if (self.loggedIn()) { + return "\"" + self.username() + "\""; + } else { + return "Login"; + } + }) + + self.requestData = function() { + $.ajax({ + url: AJAX_BASEURL + "login", + type: "POST", + data: {"passive": true}, + success: self.fromResponse + }) + } + + self.fromResponse = function(response) { + if (response && response.name) { + self.loggedIn(true); + self.username(response.name); + self.isUser(response.user); + self.isAdmin(response.admin); + } else { + self.loggedIn(false); + self.username(undefined); + self.isUser(false); + self.isAdmin(false); + } + } + + self.login = function() { + var username = $("#login_user").val(); + var password = $("#login_pass").val(); + var remember = $("#login_remember").is(":checked"); + + $.ajax({ + url: AJAX_BASEURL + "login", + type: "POST", + data: {"user": username, "pass": password, "remember": remember}, + success: function(response) { + $.pnotify({title: "Login successful", text: "You are now logged in", type: "success"}); + self.fromResponse(response); + }, + error: function(jqXHR, textStatus, errorThrown) { + $.pnotify({title: "Login failed", text: "User unknown or wrong password", type: "error"}); + } + }) + } + + self.logout = function() { + $.ajax({ + url: AJAX_BASEURL + "logout", + type: "POST", + success: function(response) { + $.pnotify({title: "Logout successful", text: "You are now logged out", type: "success"}); + self.fromResponse(response); + } + }) + } +} + +function ConnectionViewModel(loginStateViewModel) { + var self = this; + + self.loginState = loginStateViewModel; + self.portOptions = ko.observableArray(undefined); self.baudrateOptions = ko.observableArray(undefined); self.selectedPort = ko.observable(undefined); @@ -107,9 +179,11 @@ function ConnectionViewModel() { } } -function PrinterStateViewModel() { +function PrinterStateViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.stateString = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); @@ -238,9 +312,11 @@ function PrinterStateViewModel() { } } -function TemperatureViewModel(settingsViewModel) { +function TemperatureViewModel(loginStateViewModel, settingsViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.temp = ko.observable(undefined); self.bedTemp = ko.observable(undefined); self.targetTemp = ko.observable(undefined); @@ -412,9 +488,11 @@ function TemperatureViewModel(settingsViewModel) { } } -function ControlViewModel() { +function ControlViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); @@ -567,9 +645,11 @@ function ControlViewModel() { } -function SpeedViewModel() { +function SpeedViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.outerWall = ko.observable(undefined); self.innerWall = ko.observable(undefined); self.fill = ko.observable(undefined); @@ -625,9 +705,11 @@ function SpeedViewModel() { } } -function TerminalViewModel() { +function TerminalViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.log = []; self.isErrorOrClosed = ko.observable(undefined); @@ -654,6 +736,7 @@ function TerminalViewModel() { if (!self.log) self.log = [] self.log = self.log.concat(data) + self.log = self.log.slice(-300) self.updateOutput(); } @@ -690,9 +773,11 @@ function TerminalViewModel() { } } -function GcodeFilesViewModel() { +function GcodeFilesViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); @@ -824,9 +909,11 @@ function GcodeFilesViewModel() { } -function TimelapseViewModel() { +function TimelapseViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.timelapseType = ko.observable(undefined); self.timelapseTimedInterval = ko.observable(undefined); @@ -943,9 +1030,11 @@ function TimelapseViewModel() { } } -function GcodeViewModel() { +function GcodeViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.loadedFilename = undefined; self.status = 'idle'; self.enabled = false; @@ -1006,9 +1095,11 @@ function GcodeViewModel() { } -function SettingsViewModel() { +function SettingsViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.appearance_name = ko.observable(undefined); self.appearance_color = ko.observable(undefined); @@ -1135,9 +1226,10 @@ function SettingsViewModel() { } -function NavigationViewModel(appearanceViewModel, settingsViewModel) { +function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel) { var self = this; + self.loginState = loginStateViewModel; self.appearance = appearanceViewModel; self.systemActions = settingsViewModel.system_actions; @@ -1166,9 +1258,10 @@ function NavigationViewModel(appearanceViewModel, settingsViewModel) { } } -function DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, speedViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel) { +function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, speedViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel) { var self = this; + self.loginStateViewModel = loginStateViewModel; self.connectionViewModel = connectionViewModel; self.printerStateViewModel = printerStateViewModel; self.temperatureViewModel = temperatureViewModel; @@ -1185,6 +1278,7 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView $("#offline_overlay").hide(); self.timelapseViewModel.requestData(); $("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime()); + self.loginStateViewModel.requestData(); } }) self._socket.on("disconnect", function() { @@ -1480,20 +1574,22 @@ function AppearanceViewModel(settingsViewModel) { $(function() { //~~ View models - var connectionViewModel = new ConnectionViewModel(); - var printerStateViewModel = new PrinterStateViewModel(); - var settingsViewModel = new SettingsViewModel(); + var loginStateViewModel = new LoginStateViewModel(); + var connectionViewModel = new ConnectionViewModel(loginStateViewModel); + var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel); + var settingsViewModel = new SettingsViewModel(loginStateViewModel); var appearanceViewModel = new AppearanceViewModel(settingsViewModel); - var temperatureViewModel = new TemperatureViewModel(settingsViewModel); - var controlViewModel = new ControlViewModel(); - var speedViewModel = new SpeedViewModel(); - var terminalViewModel = new TerminalViewModel(); - var gcodeFilesViewModel = new GcodeFilesViewModel(); - var timelapseViewModel = new TimelapseViewModel(); - var gcodeViewModel = new GcodeViewModel(); - var navigationViewModel = new NavigationViewModel(appearanceViewModel, settingsViewModel); + var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel); + var controlViewModel = new ControlsViewModel(loginStateViewModel); + var speedViewModel = new SpeedViewModel(loginStateViewModel); + var terminalViewModel = new TerminalViewModel(loginStateViewModel); + var gcodeFilesViewModel = new GcodeFilesViewModel(loginStateViewModel); + var timelapseViewModel = new TimelapseViewModel(loginStateViewModel); + var gcodeViewModel = new GcodeViewModel(loginStateViewModel); + var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel); var dataUpdater = new DataUpdater( + loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, @@ -1505,7 +1601,8 @@ $(function() { gcodeViewModel ); - //work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at http://stackoverflow.com/questions/12506897/is-safari-on-ios-6-caching-ajax-results + // work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at + // http://stackoverflow.com/questions/12506897/is-safari-on-ios-6-caching-ajax-results $.ajaxSetup({ type: 'POST', headers: { "cache-control": "no-cache" } @@ -1547,6 +1644,7 @@ $(function() { }) $('#tabs a[data-toggle="tab"]').on('shown', function (e) { temperatureViewModel.updatePlot(); + terminalViewModel.updateOutput(); }); //~~ Speed controls @@ -1618,71 +1716,6 @@ $(function() { //~~ Offline overlay $("#offline_overlay_reconnect").click(function() {dataUpdater.reconnect()}); - //~~ Alert - - /* - function displayAlert(text, timeout, type) { - var placeholder = $("#alert_placeholder"); - - var alertType = ""; - if (type == "success" || type == "error" || type == "info") { - alertType = " alert-" + type; - } - - placeholder.append($("

" + text + "

")); - placeholder.fadeIn(); - $("#activeAlert").delay(timeout).fadeOut("slow", function() {$(this).remove(); $("#alert_placeholder").hide();}); - } - */ - - //~~ Login/logout - - $("#login_button").click(function() { - var username = $("#login_user").val(); - var password = $("#login_pass").val(); - var remember = $("#login_remember").is(":checked"); - - $.ajax({ - url: AJAX_BASEURL + "login", - type: "POST", - data: {"user": username, "pass": password, "remember": remember}, - success: function(response) { - $.pnotify({title: "Login successful", text: "You are now logged in", type: "success"}); - $("#login_dropdown_text").text("\"" + response.name + "\""); - $("#login_dropdown_loggedout").removeClass("dropdown-menu").addClass("hide"); - $("#login_dropdown_loggedin").removeClass("hide").addClass("dropdown-menu"); - }, - error: function(jqXHR, textStatus, errorThrown) { - $.pnotify({title: "Login failed", text: "User unknown or wrong password", type: "error"}); - } - }) - }); - $("#logout_button").click(function(){ - $.ajax({ - url: AJAX_BASEURL + "logout", - type: "POST", - success: function(response) { - $.pnotify({title: "Logout successful", text: "You are now logged out", type: "success"}); - $("#login_dropdown_text").text("Login"); - $("#login_dropdown_loggedin").removeClass("dropdown-menu").addClass("hide"); - $("#login_dropdown_loggedout").removeClass("hide").addClass("dropdown-menu"); - } - }) - }) - - $.ajax({ - url: AJAX_BASEURL + "login", - type: "POST", - data: {"passive": true}, - success: function(response) { - if (response["name"]) { - $("#login_dropdown_text").text("\"" + response.name + "\""); - $("#login_dropdown_loggedout").removeClass("dropdown-menu").addClass("hide"); - $("#login_dropdown_loggedin").removeClass("hide").addClass("dropdown-menu"); - } - } - }) - //~~ knockout.js bindings ko.bindingHandlers.popover = { @@ -1717,14 +1750,15 @@ $(function() { var timelapseElement = document.getElementById("timelapse"); if (timelapseElement) { - ko.applyBindings(timelapseViewModel, document.getElementById("timelapse")); + ko.applyBindings(timelapseViewModel, timelapseElement); } var gCodeVisualizerElement = document.getElementById("gcode"); - if(gCodeVisualizerElement){ + if (gCodeVisualizerElement) { gcodeViewModel.initialize(); } //~~ startup commands + loginStateViewModel.requestData(); connectionViewModel.requestData(); controlViewModel.requestData(); gcodeFilesViewModel.requestData(); @@ -1745,7 +1779,7 @@ $(function() { $.pnotify.defaults.history = false; - // Fix input element click problem + // Fix input element click problem on login dialog $('.dropdown input, .dropdown label').click(function(e) { e.stopPropagation(); }); diff --git a/octoprint/templates/index.html b/octoprint/templates/index.html index 998e345..632ed60 100644 --- a/octoprint/templates/index.html +++ b/octoprint/templates/index.html @@ -33,9 +33,13 @@ OctoPrint @@ -113,9 +115,9 @@ @@ -209,12 +211,12 @@
- + °C
- - +

General

- - - + + +
@@ -341,7 +343,7 @@ @@ -360,30 +362,30 @@
- + % - +
- + % - +
- + % - +
- + % - +
@@ -482,7 +484,7 @@
- +
{% if enableTimelapse %} @@ -490,7 +492,7 @@

Timelapse Configuration

- @@ -505,7 +507,7 @@
- +

Finished Timelapses

diff --git a/octoprint/users.py b/octoprint/users.py index d502da7..f8268f1 100644 --- a/octoprint/users.py +++ b/octoprint/users.py @@ -150,22 +150,34 @@ class UnknownRole(Exception): class User(UserMixin): def __init__(self, username, passwordHash, active, roles): - self.username = username - self.passwordHash = passwordHash - self.active = active - self.roles = roles + self._username = username + self._passwordHash = passwordHash + self._active = active + self._roles = roles + + def check_password(self, passwordHash): + return self._passwordHash == passwordHash def get_id(self): - return self.username + return self._username + + def get_name(self): + return self._username def is_active(self): - return self.active + return self._active + + def is_user(self): + return "user" in self._roles + + def is_admin(self): + return "admin" in self._roles ##~~ DummyUser object to use when accessControl is disabled -class DummyUser(UserMixin): +class DummyUser(User): def __init__(self): - self.roles = UserManager.valid_roles + User.__init__(self, "dummy", "", True, UserManager.valid_roles) - def get_id(self): - return "dummy" \ No newline at end of file + def check_password(self, passwordHash): + return True \ No newline at end of file