First work on login and user management
This commit is contained in:
parent
34a860efda
commit
150d6cb53d
5 changed files with 215 additions and 1 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
161
octoprint/users.py
Normal 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
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in a new issue