From 150d6cb53d62ad4cd8aee9834ab6fa749c500167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 17 Mar 2013 22:28:08 +0100 Subject: [PATCH] First work on login and user management --- octoprint/server.py | 26 ++++++ octoprint/static/js/ui.js | 5 + octoprint/templates/index.html | 21 +++++ octoprint/users.py | 161 +++++++++++++++++++++++++++++++++ requirements.txt | 3 +- 5 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 octoprint/users.py diff --git a/octoprint/server.py b/octoprint/server.py index aab4393c..cdc059a2 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -5,25 +5,31 @@ __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 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 import octoprint.gcodefiles as gcodefiles import octoprint.util as util +import octoprint.users as users SUCCESS = {} BASEURL = "/ajax/" + app = Flask("octoprint") # Only instantiated by the Server().run() method # In order that threads don't start too early when running as a Daemon printer = None gcodeManager = None +userManager = None #~~ Printer state @@ -442,6 +448,21 @@ def performSystemAction(): return app.make_response(("Command failed: %r" % ex, 500, [])) return jsonify(SUCCESS) +#~~ Login/user handling + +@app.route(BASEURL + "login", methods=["POST"]) +def login(): + if "user" in request.values.keys() and "pass" in request.values.keys(): + username = request.values["user"] + password = request.values["pass"] + + passwordHash = users.createPasswordHash(password) + + pass + +def load_user(userid): + pass + #~~ startup code class Server(): def __init__(self, configfile=None, basedir=None, host="0.0.0.0", port=5000, debug=False): @@ -470,6 +491,11 @@ class Server(): gcodeManager = gcodefiles.GcodeManager() printer = Printer(gcodeManager) + app.secret_key = "k3PuVYgtxNm8DXKKTw2nWmFQQun9qceV" + login_manager = LoginManager() + login_manager.session_protection = "strong" + login_manager.init_app(app) + if self._host is None: self._host = settings().get(["server", "host"]) if self._port is None: diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index 21640066..98358673 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -1680,6 +1680,11 @@ $(function() { $.pnotify.defaults.history = false; + // Fix input element click problem + $('.dropdown input, .dropdown label').click(function(e) { + e.stopPropagation(); + }); + } ); diff --git a/octoprint/templates/index.html b/octoprint/templates/index.html index dc370ee3..0a39ae91 100644 --- a/octoprint/templates/index.html +++ b/octoprint/templates/index.html @@ -45,6 +45,27 @@ {% endif %} + diff --git a/octoprint/users.py b/octoprint/users.py new file mode 100644 index 00000000..1a8ce1c6 --- /dev/null +++ b/octoprint/users.py @@ -0,0 +1,161 @@ +# coding=utf-8 +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' + +from flask.ext.login import UserMixin +import hashlib +import os +import yaml + +from octoprint.settings import settings + +class UserManager: + valid_roles=["user", "admin"] + + @staticmethod + def createPasswordHash(password): + return hashlib.sha512(password + "mvBUTvwzBzD3yPwvnJ4E4tXNf3CGJvvW").hexdigest() + + def addUser(self, username, password): + pass + + def addRoleToUser(self, username, role): + pass + + def removeRoleFromUser(self, username, role): + pass + + def updateUser(self, username, password): + pass + + def removeUser(self, username): + pass + + def findUser(self, username=None): + return None + +##~~ FilebasedUserManager, takes available users from users.yaml file + +class FilebasedUserManager(UserManager): + def __init__(self, userfile=None): + UserManager.__init__(self) + + if userfile is None: + userfile = os.path.join(settings().settings_dir, "users.yaml") + self._userfile = userfile + self._users = None + self._dirty = False + + self._load() + + def _load(self): + self._users = {} + if os.path.exists(self._userfile) and os.path.isfile(self._userfile): + with open(self._userfile, "r") as f: + data = yaml.safe_load(f) + for name in data.keys(): + attributes = data[name] + self._users[name] = User(name, attributes.password, attributes.active, attributes.roles) + + def _save(self, force=False): + if not self._dirty and not force: + return + + data = {} + for name in self._users.keys(): + user = self._users[name] + data[name] = { + "password": user.passwordHash, + "active": user.active, + "roles": user.roles + } + + with open(self._userfile, "wb") as f: + yaml.safe_dump(data, f, default_flow_style=False, indent=" ", allow_unicode=True) + self._dirty = False + self._load() + + def addUser(self, username, password): + if username in self._users.keys(): + raise UserAlreadyExists(username) + + self._users[username] = User(username, UserManager.createPasswordHash(password), False, ["user"]) + self._dirty = True + self._save() + + def addRoleToUser(self, username, role): + if not username in self._users.keys(): + raise UnknownUser(username) + + user = self._users[username] + if not role in user.roles: + user.roles.append(role) + self._dirty = True + self._save() + + def removeRoleFromUser(self, username, role): + if not username in self._users.keys(): + raise UnknownUser(username) + + user = self._users[username] + if role in user.roles: + user.roles.remove(role) + self._dirty = True + self._save() + + def updateUser(self, username, password): + if not username in self._users.keys(): + raise UnknownUser(username) + + passwordHash = UserManager.createPasswordHash(password) + user = self._users[username] + if user.passwordHash != passwordHash: + user.passwordHash = passwordHash + self._dirty = True + self._save() + + def removeUser(self, username): + if not username in self._users.keys(): + raise UnknownUser(username) + + del self._users[username] + self._dirty = True + self._save() + + def findUser(self, username=None): + if username is None: + return None + + if username not in self._users.keys(): + return None + + return self._users[username] + +##~~ Exceptions + +class UserAlreadyExists(Exception): + def __init__(self, username): + Exception.__init__(self, "User %s already exists" % username) + +class UnknownUser(Exception): + def __init__(self, username): + Exception.__init__(self, "Unknown user: %s" % username) + +class UnknownRole(Exception): + def _init_(self, role): + Exception.__init__(self, "Unknown role: %s" % role) + +##~~ User object + +class User(UserMixin): + def __init__(self, username, passwordHash, active, roles): + self.username = username + self.passwordHash = passwordHash + self.active = active + self.roles = roles + + def get_id(self): + return self.username + + def is_active(self): + return self.active \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2599a61c..4114ee78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ numpy>=1.6.2 pyserial>=2.6 tornado>=2.4.1 tornadio2>=0.0.4 -PyYAML>=3.10 \ No newline at end of file +PyYAML>=3.10 +Flask-Login>=0.1.3 \ No newline at end of file