From 874a7421e92a80f1a09e6d8f2371360e1acf2629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 18 Mar 2013 22:27:23 +0100 Subject: [PATCH] Login and logout working for the first time --- octoprint/server.py | 61 ++++++++++++++++++++++++------- octoprint/settings.py | 5 +++ octoprint/static/js/ui.js | 65 ++++++++++++++++++++++++++++++++++ octoprint/templates/index.html | 4 ++- octoprint/timelapse.py | 2 +- octoprint/users.py | 20 ++++++++--- octoprint/util/__init__.py | 11 ++++++ 7 files changed, 149 insertions(+), 19 deletions(-) diff --git a/octoprint/server.py b/octoprint/server.py index cdc059a..c2384cd 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -5,15 +5,13 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp from flask import Flask, request, render_template, jsonify, send_from_directory, abort, url_for from werkzeug.utils import secure_filename import tornadio2 -from flask.ext.login import LoginManager +from flask.ext.login import LoginManager, login_user, logout_user, login_required, current_user, AnonymousUser import os import threading import logging, logging.config import subprocess -import hashlib - from octoprint.printer import Printer, getConnectionOptions from octoprint.settings import settings, valid_boolean_trues import octoprint.timelapse as timelapse @@ -109,7 +107,8 @@ def index(): webcamStream=settings().get(["webcam", "stream"]), enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None), enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]), - enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0 + enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0, + enableAccessControl=userManager is not None ) #~~ Printer control @@ -119,6 +118,7 @@ def connectionOptions(): return jsonify(getConnectionOptions()) @app.route(BASEURL + "control/connect", methods=["POST"]) +@login_required def connect(): port = None baudrate = None @@ -456,12 +456,36 @@ def login(): username = request.values["user"] password = request.values["pass"] - passwordHash = users.createPasswordHash(password) + if "remember" in request.values.keys() and request.values["remember"]: + remember = True + else: + remember = False - pass + user = userManager.findUser(username) + if user is not None: + passwordHash = users.UserManager.createPasswordHash(password) + if passwordHash == user.passwordHash: + login_user(user, remember=remember) + return jsonify({"name": user.username, "roles": user.roles}) + 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}) + else: + return jsonify(SUCCESS) -def load_user(userid): - pass +@app.route(BASEURL + "logout", methods=["POST"]) +@login_required +def logout(): + logout_user() + return jsonify(SUCCESS) + +def load_user(id): + if userManager is not None: + return userManager.findUser(id) + else: + return users.DummyUser() #~~ startup code class Server(): @@ -476,6 +500,7 @@ class Server(): # Global as I can't work out a way to get it into PrinterStateConnection global printer global gcodeManager + global userManager from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer @@ -487,13 +512,25 @@ class Server(): # then initialize logging self._initLogging(self._debug) + logger = logging.getLogger(__name__) gcodeManager = gcodefiles.GcodeManager() printer = Printer(gcodeManager) + if settings().getBoolean(["accessControl", "enabled"]): + userManagerName = settings().get(["accessControl", "userManager"]) + try: + clazz = util.getClass(userManagerName) + userManager = clazz() + except AttributeError, e: + logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName) + app.secret_key = "k3PuVYgtxNm8DXKKTw2nWmFQQun9qceV" login_manager = LoginManager() login_manager.session_protection = "strong" + login_manager.user_callback = load_user + if userManager is None: + login_manager.anonymous_user = users.DummyUser login_manager.init_app(app) if self._host is None: @@ -501,7 +538,7 @@ class Server(): if self._port is None: self._port = settings().getInt(["server", "port"]) - logging.getLogger(__name__).info("Listening on http://%s:%d" % (self._host, self._port)) + logger.info("Listening on http://%s:%d" % (self._host, self._port)) app.debug = self._debug self._router = tornadio2.TornadioRouter(PrinterStateConnection) @@ -517,7 +554,7 @@ class Server(): s = settings(init=True, basedir=basedir, configfile=configfile) def _initLogging(self, debug): - self._config = { + config = { "version": 1, "formatters": { "simple": { @@ -556,13 +593,13 @@ class Server(): } if debug: - self._config["loggers"]["SERIAL"] = { + config["loggers"]["SERIAL"] = { "level": "DEBUG", "handlers": ["serialFile"], "propagate": False } - logging.config.dictConfig(self._config) + logging.config.dictConfig(config) if __name__ == "__main__": octoprint = Server() diff --git a/octoprint/settings.py b/octoprint/settings.py index 0c7dfad..7c385f8 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -69,6 +69,11 @@ default_settings = { "controls": [], "system": { "actions": [] + }, + "accessControl": { + "enabled": False, + "userManager": "octoprint.users.FilebasedUserManager", + "userfile": None } } diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index 9835867..9cf01a3 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -1618,6 +1618,71 @@ $(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 = { diff --git a/octoprint/templates/index.html b/octoprint/templates/index.html index 0a39ae9..998e345 100644 --- a/octoprint/templates/index.html +++ b/octoprint/templates/index.html @@ -45,6 +45,7 @@ {% endif %} + {% if enableAccessControl %} + {% endif %} diff --git a/octoprint/timelapse.py b/octoprint/timelapse.py index 4283323..2db8984 100644 --- a/octoprint/timelapse.py +++ b/octoprint/timelapse.py @@ -83,7 +83,7 @@ class Timelapse(object): with self._captureMutex: filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber)) - self._imageNumber += 1; + self._imageNumber += 1 self._logger.debug("Capturing image to %s" % filename) captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename}) diff --git a/octoprint/users.py b/octoprint/users.py index 1a8ce1c..d502da7 100644 --- a/octoprint/users.py +++ b/octoprint/users.py @@ -9,8 +9,8 @@ import yaml from octoprint.settings import settings -class UserManager: - valid_roles=["user", "admin"] +class UserManager(object): + valid_roles = ["user", "admin"] @staticmethod def createPasswordHash(password): @@ -37,9 +37,10 @@ class UserManager: ##~~ FilebasedUserManager, takes available users from users.yaml file class FilebasedUserManager(UserManager): - def __init__(self, userfile=None): + def __init__(self): UserManager.__init__(self) + userfile = settings().get(["accessControl", "userfile"]) if userfile is None: userfile = os.path.join(settings().settings_dir, "users.yaml") self._userfile = userfile @@ -49,7 +50,7 @@ class FilebasedUserManager(UserManager): self._load() def _load(self): - self._users = {} + self._users = {"admin": User("admin", "7557160613d5258f883014a7c3c0428de53040fc152b1791f1cc04a62b428c0c2a9c46ed330cdce9689353ab7a5352ba2b2ceb459b96e9c8ed7d0cb0b2c0c076", True, UserManager.valid_roles)} if os.path.exists(self._userfile) and os.path.isfile(self._userfile): with open(self._userfile, "r") as f: data = yaml.safe_load(f) @@ -158,4 +159,13 @@ class User(UserMixin): return self.username def is_active(self): - return self.active \ No newline at end of file + return self.active + +##~~ DummyUser object to use when accessControl is disabled + +class DummyUser(UserMixin): + def __init__(self): + self.roles = UserManager.valid_roles + + def get_id(self): + return "dummy" \ No newline at end of file diff --git a/octoprint/util/__init__.py b/octoprint/util/__init__.py index 52d8361..f762414 100644 --- a/octoprint/util/__init__.py +++ b/octoprint/util/__init__.py @@ -28,3 +28,14 @@ def getFormattedDateTime(d): return None return d.strftime("%Y-%m-%d %H:%M") + +def getClass(name): + """ + Taken from http://stackoverflow.com/a/452981/2028598 + """ + parts = name.split(".") + module = ".".join(parts[:-1]) + m = __import__(module) + for comp in parts[1:]: + m = getattr(m, comp) + return m \ No newline at end of file