First work on login and user management

This commit is contained in:
Gina Häußge 2013-03-17 22:28:08 +01:00
parent 34a860efda
commit 150d6cb53d
5 changed files with 215 additions and 1 deletions

View file

@ -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 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
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
import octoprint.gcodefiles as gcodefiles import octoprint.gcodefiles as gcodefiles
import octoprint.util as util import octoprint.util as util
import octoprint.users as users
SUCCESS = {} SUCCESS = {}
BASEURL = "/ajax/" BASEURL = "/ajax/"
app = Flask("octoprint") app = Flask("octoprint")
# Only instantiated by the Server().run() method # Only instantiated by the Server().run() method
# In order that threads don't start too early when running as a Daemon # In order that threads don't start too early when running as a Daemon
printer = None printer = None
gcodeManager = None gcodeManager = None
userManager = None
#~~ Printer state #~~ Printer state
@ -442,6 +448,21 @@ def performSystemAction():
return app.make_response(("Command failed: %r" % ex, 500, [])) return app.make_response(("Command failed: %r" % ex, 500, []))
return jsonify(SUCCESS) 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 #~~ startup code
class Server(): class Server():
def __init__(self, configfile=None, basedir=None, host="0.0.0.0", port=5000, debug=False): 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() gcodeManager = gcodefiles.GcodeManager()
printer = Printer(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: if self._host is None:
self._host = settings().get(["server", "host"]) self._host = settings().get(["server", "host"])
if self._port is None: if self._port is None:

View file

@ -1680,6 +1680,11 @@ $(function() {
$.pnotify.defaults.history = false; $.pnotify.defaults.history = false;
// Fix input element click problem
$('.dropdown input, .dropdown label').click(function(e) {
e.stopPropagation();
});
} }
); );

View file

@ -45,6 +45,27 @@
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-user"></i> <span id="login_dropdown_text">Login</span>
<b class="caret"></b>
</a>
<div id="login_dropdown_loggedout" class="dropdown-menu" style="padding: 15px">
<label for="login_user">Username</label>
<input type="text" id="login_user" placeholder="Username">
<label for="login_pass">Password</label>
<input type="password" id="login_pass" placeholder="Password">
<label class="checkbox">
<input type="checkbox" id="login_remember"> Remember me
</label>
<button class="btn btn-block btn-primary" id="login_button">Login</button>
</div>
<ul id="login_dropdown_loggedin" class="hide">
<li><a href="#">Profile</a></li>
<li class="divider"></li>
<li><a href="#">Logout</a></li>
</ul>
</li>
</ul> </ul>
</div> </div>
</div> </div>

161
octoprint/users.py Normal file
View file

@ -0,0 +1,161 @@
# coding=utf-8
__author__ = "Gina Häußge <osd@foosel.net>"
__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

View file

@ -3,4 +3,5 @@ numpy>=1.6.2
pyserial>=2.6 pyserial>=2.6
tornado>=2.4.1 tornado>=2.4.1
tornadio2>=0.0.4 tornadio2>=0.0.4
PyYAML>=3.10 PyYAML>=3.10
Flask-Login>=0.1.3