Login and logout working for the first time

master
Gina Häußge 2013-03-18 22:27:23 +01:00
parent 150d6cb53d
commit 874a7421e9
7 changed files with 149 additions and 19 deletions

View File

@ -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 flask import Flask, request, render_template, jsonify, send_from_directory, abort, url_for
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
import tornadio2 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 os
import threading import threading
import logging, logging.config import logging, logging.config
import subprocess import subprocess
import hashlib
from octoprint.printer import Printer, getConnectionOptions from octoprint.printer import Printer, getConnectionOptions
from octoprint.settings import settings, valid_boolean_trues from octoprint.settings import settings, valid_boolean_trues
import octoprint.timelapse as timelapse import octoprint.timelapse as timelapse
@ -109,7 +107,8 @@ def index():
webcamStream=settings().get(["webcam", "stream"]), webcamStream=settings().get(["webcam", "stream"]),
enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None), enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None),
enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]), 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 #~~ Printer control
@ -119,6 +118,7 @@ def connectionOptions():
return jsonify(getConnectionOptions()) return jsonify(getConnectionOptions())
@app.route(BASEURL + "control/connect", methods=["POST"]) @app.route(BASEURL + "control/connect", methods=["POST"])
@login_required
def connect(): def connect():
port = None port = None
baudrate = None baudrate = None
@ -456,12 +456,36 @@ def login():
username = request.values["user"] username = request.values["user"]
password = request.values["pass"] 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): @app.route(BASEURL + "logout", methods=["POST"])
pass @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 #~~ startup code
class Server(): class Server():
@ -476,6 +500,7 @@ class Server():
# Global as I can't work out a way to get it into PrinterStateConnection # Global as I can't work out a way to get it into PrinterStateConnection
global printer global printer
global gcodeManager global gcodeManager
global userManager
from tornado.wsgi import WSGIContainer from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
@ -487,13 +512,25 @@ class Server():
# then initialize logging # then initialize logging
self._initLogging(self._debug) self._initLogging(self._debug)
logger = logging.getLogger(__name__)
gcodeManager = gcodefiles.GcodeManager() gcodeManager = gcodefiles.GcodeManager()
printer = Printer(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" app.secret_key = "k3PuVYgtxNm8DXKKTw2nWmFQQun9qceV"
login_manager = LoginManager() login_manager = LoginManager()
login_manager.session_protection = "strong" 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) login_manager.init_app(app)
if self._host is None: if self._host is None:
@ -501,7 +538,7 @@ class Server():
if self._port is None: if self._port is None:
self._port = settings().getInt(["server", "port"]) 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 app.debug = self._debug
self._router = tornadio2.TornadioRouter(PrinterStateConnection) self._router = tornadio2.TornadioRouter(PrinterStateConnection)
@ -517,7 +554,7 @@ class Server():
s = settings(init=True, basedir=basedir, configfile=configfile) s = settings(init=True, basedir=basedir, configfile=configfile)
def _initLogging(self, debug): def _initLogging(self, debug):
self._config = { config = {
"version": 1, "version": 1,
"formatters": { "formatters": {
"simple": { "simple": {
@ -556,13 +593,13 @@ class Server():
} }
if debug: if debug:
self._config["loggers"]["SERIAL"] = { config["loggers"]["SERIAL"] = {
"level": "DEBUG", "level": "DEBUG",
"handlers": ["serialFile"], "handlers": ["serialFile"],
"propagate": False "propagate": False
} }
logging.config.dictConfig(self._config) logging.config.dictConfig(config)
if __name__ == "__main__": if __name__ == "__main__":
octoprint = Server() octoprint = Server()

View File

@ -69,6 +69,11 @@ default_settings = {
"controls": [], "controls": [],
"system": { "system": {
"actions": [] "actions": []
},
"accessControl": {
"enabled": False,
"userManager": "octoprint.users.FilebasedUserManager",
"userfile": None
} }
} }

View File

@ -1618,6 +1618,71 @@ $(function() {
//~~ Offline overlay //~~ Offline overlay
$("#offline_overlay_reconnect").click(function() {dataUpdater.reconnect()}); $("#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($("<div id='activeAlert' class='alert " + alertType + " fade in' data-alert='alert'><p>" + text + "</p></div>"));
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 //~~ knockout.js bindings
ko.bindingHandlers.popover = { ko.bindingHandlers.popover = {

View File

@ -45,6 +45,7 @@
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
{% if enableAccessControl %}
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-user"></i> <span id="login_dropdown_text">Login</span> <i class="icon-user"></i> <span id="login_dropdown_text">Login</span>
@ -63,9 +64,10 @@
<ul id="login_dropdown_loggedin" class="hide"> <ul id="login_dropdown_loggedin" class="hide">
<li><a href="#">Profile</a></li> <li><a href="#">Profile</a></li>
<li class="divider"></li> <li class="divider"></li>
<li><a href="#">Logout</a></li> <li><a href="#" id="logout_button">Logout</a></li>
</ul> </ul>
</li> </li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -83,7 +83,7 @@ class Timelapse(object):
with self._captureMutex: with self._captureMutex:
filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber)) 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) self._logger.debug("Capturing image to %s" % filename)
captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename}) captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename})

View File

@ -9,8 +9,8 @@ import yaml
from octoprint.settings import settings from octoprint.settings import settings
class UserManager: class UserManager(object):
valid_roles=["user", "admin"] valid_roles = ["user", "admin"]
@staticmethod @staticmethod
def createPasswordHash(password): def createPasswordHash(password):
@ -37,9 +37,10 @@ class UserManager:
##~~ FilebasedUserManager, takes available users from users.yaml file ##~~ FilebasedUserManager, takes available users from users.yaml file
class FilebasedUserManager(UserManager): class FilebasedUserManager(UserManager):
def __init__(self, userfile=None): def __init__(self):
UserManager.__init__(self) UserManager.__init__(self)
userfile = settings().get(["accessControl", "userfile"])
if userfile is None: if userfile is None:
userfile = os.path.join(settings().settings_dir, "users.yaml") userfile = os.path.join(settings().settings_dir, "users.yaml")
self._userfile = userfile self._userfile = userfile
@ -49,7 +50,7 @@ class FilebasedUserManager(UserManager):
self._load() self._load()
def _load(self): 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): if os.path.exists(self._userfile) and os.path.isfile(self._userfile):
with open(self._userfile, "r") as f: with open(self._userfile, "r") as f:
data = yaml.safe_load(f) data = yaml.safe_load(f)
@ -158,4 +159,13 @@ class User(UserMixin):
return self.username return self.username
def is_active(self): def is_active(self):
return self.active 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"

View File

@ -28,3 +28,14 @@ def getFormattedDateTime(d):
return None return None
return d.strftime("%Y-%m-%d %H:%M") 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