Enforces a new first-run setup wizard for access control to be run and forbids running OctoPrint as root unless a special command option is supplied

The dialog also informs about the risk of unauthorized strangers (mis)using the printer if an unsecured OctoPrint installation is made available on the internet.
master
Gina Häußge 2013-08-10 21:59:05 +02:00
parent 3b3bb36377
commit 4cf041aaad
7 changed files with 237 additions and 32 deletions

View File

@ -8,6 +8,8 @@ from flask import Flask, request, render_template, jsonify, send_from_directory,
from flask.ext.login import LoginManager, login_user, logout_user, login_required, current_user from flask.ext.login import LoginManager, login_user, logout_user, login_required, current_user
from flask.ext.principal import Principal, Permission, RoleNeed, Identity, identity_changed, AnonymousIdentity, identity_loaded, UserNeed from flask.ext.principal import Principal, Permission, RoleNeed, Identity, identity_changed, AnonymousIdentity, identity_loaded, UserNeed
from functools import wraps
import os import os
import threading import threading
import logging, logging.config import logging, logging.config
@ -35,6 +37,7 @@ timelapse = None
gcodeManager = None gcodeManager = None
userManager = None userManager = None
eventManager = None eventManager = None
loginManager = None
principals = Principal(app) principals = Principal(app)
admin_permission = Permission(RoleNeed("admin")) admin_permission = Permission(RoleNeed("admin"))
@ -126,6 +129,26 @@ class PrinterStateConnection(tornadio2.SocketConnection):
def _onMovieDone(self, event, payload): def _onMovieDone(self, event, payload):
self.sendUpdateTrigger("timelapseFiles") self.sendUpdateTrigger("timelapseFiles")
def restricted_access(func):
"""
If you decorate a view with this, it will ensure that first setup has been
done for OctoPrint's Access Control plus that any conditions of the
login_required decorator are met.
If OctoPrint's Access Control has not been setup yet (indicated by the "firstRun"
flag from the settings being set to True and the userManager not indicating
that it's user database has been customized from default), the decorator
will cause a HTTP 403 status code to be returned by the decorated resource.
Otherwise the result of calling login_required will be returned.
"""
@wraps(func)
def decorated_view(*args, **kwargs):
if settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()):
return make_response("OctoPrint isn't setup yet", 403)
return login_required(func)(*args, **kwargs)
return decorated_view
# Did attempt to make webserver an encapsulated class but ended up with __call__ failures # Did attempt to make webserver an encapsulated class but ended up with __call__ failures
@app.route("/") @app.route("/")
@ -146,6 +169,7 @@ def index():
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, enableAccessControl=userManager is not None,
enableSdSupport=settings().get(["feature", "sdSupport"]), enableSdSupport=settings().get(["feature", "sdSupport"]),
firstRun=settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()),
gitBranch=branch, gitBranch=branch,
gitCommit=commit gitCommit=commit
) )
@ -157,7 +181,7 @@ def connectionOptions():
return jsonify(getConnectionOptions()) return jsonify(getConnectionOptions())
@app.route(BASEURL + "control/connection", methods=["POST"]) @app.route(BASEURL + "control/connection", methods=["POST"])
@login_required @restricted_access
def connect(): def connect():
if "command" in request.values.keys() and request.values["command"] == "connect": if "command" in request.values.keys() and request.values["command"] == "connect":
port = None port = None
@ -180,7 +204,7 @@ def connect():
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "control/command", methods=["POST"]) @app.route(BASEURL + "control/command", methods=["POST"])
@login_required @restricted_access
def printerCommand(): def printerCommand():
if "application/json" in request.headers["Content-Type"]: if "application/json" in request.headers["Content-Type"]:
data = request.json data = request.json
@ -204,7 +228,7 @@ def printerCommand():
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "control/job", methods=["POST"]) @app.route(BASEURL + "control/job", methods=["POST"])
@login_required @restricted_access
def printJobControl(): def printJobControl():
if "command" in request.values.keys(): if "command" in request.values.keys():
if request.values["command"] == "start": if request.values["command"] == "start":
@ -216,7 +240,7 @@ def printJobControl():
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "control/temperature", methods=["POST"]) @app.route(BASEURL + "control/temperature", methods=["POST"])
@login_required @restricted_access
def setTargetTemperature(): def setTargetTemperature():
if "temp" in request.values.keys(): if "temp" in request.values.keys():
# set target temperature # set target temperature
@ -231,7 +255,7 @@ def setTargetTemperature():
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "control/jog", methods=["POST"]) @app.route(BASEURL + "control/jog", methods=["POST"])
@login_required @restricted_access
def jog(): def jog():
if not printer.isOperational() or printer.isPrinting(): if not printer.isOperational() or printer.isPrinting():
# do not jog when a print job is running or we don't have a connection # do not jog when a print job is running or we don't have a connection
@ -269,7 +293,7 @@ def getCustomControls():
return jsonify(controls=customControls) return jsonify(controls=customControls)
@app.route(BASEURL + "control/sd", methods=["POST"]) @app.route(BASEURL + "control/sd", methods=["POST"])
@login_required @restricted_access
def sdCommand(): def sdCommand():
if not settings().getBoolean(["feature", "sdSupport"]) or not printer.isOperational() or printer.isPrinting(): if not settings().getBoolean(["feature", "sdSupport"]) or not printer.isOperational() or printer.isPrinting():
return jsonify(SUCCESS) return jsonify(SUCCESS)
@ -308,7 +332,7 @@ def readGcodeFile(filename):
return send_from_directory(settings().getBaseFolder("uploads"), filename, as_attachment=True) return send_from_directory(settings().getBaseFolder("uploads"), filename, as_attachment=True)
@app.route(BASEURL + "gcodefiles/upload", methods=["POST"]) @app.route(BASEURL + "gcodefiles/upload", methods=["POST"])
@login_required @restricted_access
def uploadGcodeFile(): def uploadGcodeFile():
if "gcode_file" in request.files.keys(): if "gcode_file" in request.files.keys():
file = request.files["gcode_file"] file = request.files["gcode_file"]
@ -350,7 +374,7 @@ def uploadGcodeFile():
@app.route(BASEURL + "gcodefiles/load", methods=["POST"]) @app.route(BASEURL + "gcodefiles/load", methods=["POST"])
@login_required @restricted_access
def loadGcodeFile(): def loadGcodeFile():
if "filename" in request.values.keys(): if "filename" in request.values.keys():
printAfterLoading = False printAfterLoading = False
@ -367,7 +391,7 @@ def loadGcodeFile():
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"]) @app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
@login_required @restricted_access
def deleteGcodeFile(): def deleteGcodeFile():
if "filename" in request.values.keys(): if "filename" in request.values.keys():
filename = request.values["filename"] filename = request.values["filename"]
@ -391,7 +415,7 @@ def deleteGcodeFile():
return readGcodeFiles() return readGcodeFiles()
@app.route(BASEURL + "gcodefiles/refresh", methods=["POST"]) @app.route(BASEURL + "gcodefiles/refresh", methods=["POST"])
@login_required @restricted_access
def refreshFiles(): def refreshFiles():
printer.updateSdFiles() printer.updateSdFiles()
return jsonify(SUCCESS) return jsonify(SUCCESS)
@ -478,7 +502,7 @@ def downloadTimelapse(filename):
return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True) return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True)
@app.route(BASEURL + "timelapse/<filename>", methods=["DELETE"]) @app.route(BASEURL + "timelapse/<filename>", methods=["DELETE"])
@login_required @restricted_access
def deleteTimelapse(filename): def deleteTimelapse(filename):
if util.isAllowedFile(filename, set(["mpg"])): if util.isAllowedFile(filename, set(["mpg"])):
secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename)) secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename))
@ -487,7 +511,7 @@ def deleteTimelapse(filename):
return getTimelapseData() return getTimelapseData()
@app.route(BASEURL + "timelapse", methods=["POST"]) @app.route(BASEURL + "timelapse", methods=["POST"])
@login_required @restricted_access
def setTimelapseConfig(): def setTimelapseConfig():
global timelapse global timelapse
@ -578,7 +602,7 @@ def getSettings():
}) })
@app.route(BASEURL + "settings", methods=["POST"]) @app.route(BASEURL + "settings", methods=["POST"])
@login_required @restricted_access
@admin_permission.require(403) @admin_permission.require(403)
def setSettings(): def setSettings():
if "application/json" in request.headers["Content-Type"]: if "application/json" in request.headers["Content-Type"]:
@ -649,10 +673,36 @@ def setSettings():
return getSettings() return getSettings()
@app.route(BASEURL + "setup", methods=["POST"])
def firstRunSetup():
global userManager
if not settings().getBoolean(["server", "firstRun"]):
abort(403)
if "ac" in request.values.keys() and request.values["ac"] in valid_boolean_trues and \
"user" in request.values.keys() and "pass1" in request.values.keys() and \
"pass2" in request.values.keys() and request.values["pass1"] == request.values["pass2"]:
# configure access control
settings().setBoolean(["accessControl", "enabled"], True)
userManager.addUser(request.values["user"], request.values["pass1"], True, ["user", "admin"])
settings().setBoolean(["server", "firstRun"], False)
elif "ac" in request.values.keys() and not request.values["ac"] in valid_boolean_trues:
# disable access control
settings().setBoolean(["accessControl", "enabled"], False)
settings().setBoolean(["server", "firstRun"], False)
userManager = None
loginManager.anonymous_user = users.DummyUser
principals.identity_loaders.appendleft(users.dummy_identity_loader)
settings().save()
return jsonify(SUCCESS)
#~~ user settings #~~ user settings
@app.route(BASEURL + "users", methods=["GET"]) @app.route(BASEURL + "users", methods=["GET"])
@login_required @restricted_access
@admin_permission.require(403) @admin_permission.require(403)
def getUsers(): def getUsers():
if userManager is None: if userManager is None:
@ -661,7 +711,7 @@ def getUsers():
return jsonify({"users": userManager.getAllUsers()}) return jsonify({"users": userManager.getAllUsers()})
@app.route(BASEURL + "users", methods=["POST"]) @app.route(BASEURL + "users", methods=["POST"])
@login_required @restricted_access
@admin_permission.require(403) @admin_permission.require(403)
def addUser(): def addUser():
if userManager is None: if userManager is None:
@ -685,7 +735,7 @@ def addUser():
return getUsers() return getUsers()
@app.route(BASEURL + "users/<username>", methods=["GET"]) @app.route(BASEURL + "users/<username>", methods=["GET"])
@login_required @restricted_access
def getUser(username): def getUser(username):
if userManager is None: if userManager is None:
return jsonify(SUCCESS) return jsonify(SUCCESS)
@ -700,7 +750,7 @@ def getUser(username):
abort(403) abort(403)
@app.route(BASEURL + "users/<username>", methods=["PUT"]) @app.route(BASEURL + "users/<username>", methods=["PUT"])
@login_required @restricted_access
@admin_permission.require(403) @admin_permission.require(403)
def updateUser(username): def updateUser(username):
if userManager is None: if userManager is None:
@ -725,7 +775,7 @@ def updateUser(username):
abort(404) abort(404)
@app.route(BASEURL + "users/<username>", methods=["DELETE"]) @app.route(BASEURL + "users/<username>", methods=["DELETE"])
@login_required @restricted_access
@admin_permission.require(http_exception=403) @admin_permission.require(http_exception=403)
def removeUser(username): def removeUser(username):
if userManager is None: if userManager is None:
@ -738,7 +788,7 @@ def removeUser(username):
abort(404) abort(404)
@app.route(BASEURL + "users/<username>/password", methods=["PUT"]) @app.route(BASEURL + "users/<username>/password", methods=["PUT"])
@login_required @restricted_access
def changePasswordForUser(username): def changePasswordForUser(username):
if userManager is None: if userManager is None:
return jsonify(SUCCESS) return jsonify(SUCCESS)
@ -758,7 +808,7 @@ def changePasswordForUser(username):
#~~ system control #~~ system control
@app.route(BASEURL + "system", methods=["POST"]) @app.route(BASEURL + "system", methods=["POST"])
@login_required @restricted_access
@admin_permission.require(403) @admin_permission.require(403)
def performSystemAction(): def performSystemAction():
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -806,7 +856,7 @@ def login():
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "logout", methods=["POST"]) @app.route(BASEURL + "logout", methods=["POST"])
@login_required @restricted_access
def logout(): def logout():
# Remove session keys set by Flask-Principal # Remove session keys set by Flask-Principal
for key in ('identity.id', 'identity.auth_type'): for key in ('identity.id', 'identity.auth_type'):
@ -836,20 +886,25 @@ def load_user(id):
#~~ 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, allowRoot=False):
self._configfile = configfile self._configfile = configfile
self._basedir = basedir self._basedir = basedir
self._host = host self._host = host
self._port = port self._port = port
self._debug = debug self._debug = debug
self._allowRoot = allowRoot
def run(self): def run(self):
if not self._allowRoot:
self._checkForRoot()
# 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 global userManager
global eventManager global eventManager
global loginManager
from tornado.wsgi import WSGIContainer from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
@ -882,13 +937,13 @@ class Server():
logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName) 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() loginManager = LoginManager()
login_manager.session_protection = "strong" loginManager.session_protection = "strong"
login_manager.user_callback = load_user loginManager.user_callback = load_user
if userManager is None: if userManager is None:
login_manager.anonymous_user = users.DummyUser loginManager.anonymous_user = users.DummyUser
principals.identity_loaders.appendleft(users.dummy_identity_loader) principals.identity_loaders.appendleft(users.dummy_identity_loader)
login_manager.init_app(app) loginManager.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"])
@ -918,6 +973,10 @@ class Server():
global printer, gcodeManager, userManager, eventManager global printer, gcodeManager, userManager, eventManager
return PrinterStateConnection(printer, gcodeManager, userManager, eventManager, session, endpoint) return PrinterStateConnection(printer, gcodeManager, userManager, eventManager, session, endpoint)
def _checkForRoot(self):
if "geteuid" in dir(os) and os.geteuid() == 0:
exit("You should not run OctoPrint as root!")
def _initSettings(self, configfile, basedir): def _initSettings(self, configfile, basedir):
s = settings(init=True, basedir=basedir, configfile=configfile) s = settings(init=True, basedir=basedir, configfile=configfile)

View File

@ -36,7 +36,8 @@ default_settings = {
}, },
"server": { "server": {
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 5000 "port": 5000,
"firstRun": True
}, },
"webcam": { "webcam": {
"stream": None, "stream": None,
@ -85,7 +86,7 @@ default_settings = {
"actions": [] "actions": []
}, },
"accessControl": { "accessControl": {
"enabled": False, "enabled": True,
"userManager": "octoprint.users.FilebasedUserManager", "userManager": "octoprint.users.FilebasedUserManager",
"userfile": None "userfile": None
}, },

View File

@ -1946,6 +1946,80 @@ function AppearanceViewModel(settingsViewModel) {
}) })
} }
function FirstRunViewModel() {
var self = this;
self.username = ko.observable(undefined);
self.password = ko.observable(undefined);
self.confirmedPassword = ko.observable(undefined);
self.passwordMismatch = ko.computed(function() {
return self.password() != self.confirmedPassword();
});
self.validUsername = ko.computed(function() {
return self.username() && self.username().trim() != "";
});
self.validPassword = ko.computed(function() {
return self.password() && self.password().trim() != "";
});
self.validData = ko.computed(function() {
return !self.passwordMismatch() && self.validUsername() && self.validPassword();
});
self.keepAccessControl = function() {
var data = {
"ac": true,
"user": self.username(),
"pass1": self.password(),
"pass2": self.confirmedPassword()
};
self._sendData(data);
};
self.disableAccessControl = function() {
$("#confirmation_dialog .confirmation_dialog_message").html("If you disable Access Control <strong>and</strong> your OctoPrint " +
"installation is accessible from the internet, your printer <strong>will be accessible by everyone - " +
"that also includes the bad guys!</strong>");
$("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {
e.preventDefault();
$("#confirmation_dialog").modal("hide");
var data = {
"ac": false
};
self._sendData(data, function() {
// if the user indeed disables access control, we'll need to reload the page for this to take effect
location.reload();
});
});
$("#confirmation_dialog").modal("show");
};
self._sendData = function(data, callback) {
$.ajax({
url: AJAX_BASEURL + "setup",
type: "POST",
dataType: "json",
data: data,
success: function() {
self.closeDialog();
if (callback) callback();
}
});
}
self.showDialog = function() {
$("#first_run_dialog").modal("show");
}
self.closeDialog = function() {
$("#first_run_dialog").modal("hide");
}
}
$(function() { $(function() {
//~~ View models //~~ View models
@ -2242,6 +2316,12 @@ $(function() {
$(document).bind("drop dragover", function (e) { $(document).bind("drop dragover", function (e) {
e.preventDefault(); e.preventDefault();
}); });
if (CONFIG_FIRST_RUN) {
var firstRunViewModel = new FirstRunViewModel();
ko.applyBindings(firstRunViewModel, document.getElementById("first_run_dialog"));
firstRunViewModel.showDialog();
}
} }
); );

View File

@ -1,4 +1,4 @@
<div id="offline_overlay"> <div id="offline_overlay" xmlns="http://www.w3.org/1999/html">
<div id="offline_overlay_background"></div> <div id="offline_overlay_background"></div>
<div id="offline_overlay_wrapper"> <div id="offline_overlay_wrapper">
<div class="container"> <div class="container">
@ -45,4 +45,56 @@
<a href="#" class="btn" data-dismiss="modal" aria-hidden="true">Cancel</a> <a href="#" class="btn" data-dismiss="modal" aria-hidden="true">Cancel</a>
<a href="#" class="btn btn-danger confirmation_dialog_acknowledge">Proceed</a> <a href="#" class="btn btn-danger confirmation_dialog_acknowledge">Proceed</a>
</div> </div>
</div>
<div id="first_run_dialog" class="modal hide fade" data-backdrop="static" data-keyboard="false">
<div class="modal-header">
<h3><i class="icon-warning-sign"></i> Configure Access Control</h3>
</div>
<div class="modal-body">
<p>
OctoPrint by default now ships with Access Control enabled, meaning you won't be able to do anything with the
printer unless you login first as a configured user. This is to prevent strangers (possibly with
malicious intent) to gain access to printer for using it in such a way that it may be damaged or worse, in case
you make your OctoPrint installation accessible to the internet or other untrustworthy computer networks.
</p>
<div>
<p>
It looks like you haven't configured access control yet. Please set up the username and password for the
initial administrator account who will have full access to both the printer and OctoPrint's settings:
</p>
<form class="form-horizontal">
<div class="control-group" data-bind="css: {success: validUsername()}">
<label class="control-label" for="first_run_username">Username</label>
<div class="controls">
<input type="text" class="input-medium" data-bind="value: username, valueUpdate: 'afterkeydown'">
</div>
</div>
<div class="control-group" data-bind="css: {success: validPassword()}">
<label class="control-label" for="first_run_username">Password</label>
<div class="controls">
<input type="password" class="input-medium" data-bind="value: password, valueUpdate: 'afterkeydown'">
</div>
</div>
<div class="control-group" data-bind="css: {error: passwordMismatch(), success: validPassword() && !passwordMismatch()}">
<label class="control-label" for="first_run_username">Confirm Password</label>
<div class="controls">
<input type="password" class="input-medium" data-bind="value: confirmedPassword, valueUpdate: 'afterkeydown'">
<span class="help-inline" data-bind="visible: passwordMismatch()">Passwords don't match</span>
</div>
</div>
</form>
</div>
<p>
In case that your OctoPrint installation is only accessible from within a network only trustworthy people have
access to and you don't need Access Control for other reasons, you may alternatively disable Access Control
all together. You should really only do this if you are absolutely sure that only people you know and trust
will be able to connect to it. To reiterate, do <strong>NOT</strong> underestimate the risk of an unsecured
access to your printer!
</p>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-danger" data-bind="click: disableAccessControl">Disable Access Control</a>
<a href="#" class="btn btn-primary" data-bind="click: keepAccessControl, enable: validData()">Keep Access Control Enabled</a>
</div>
</div> </div>

View File

@ -25,6 +25,7 @@
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}"; var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %} var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %}
var CONFIG_SD_SUPPORT = {% if enableSdSupport -%} true; {% else %} false; {%- endif %} var CONFIG_SD_SUPPORT = {% if enableSdSupport -%} true; {% else %} false; {%- endif %}
var CONFIG_FIRST_RUN = {% if firstRun -%} true; {% else %} false; {%- endif %}
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/socket.io/WebSocketMain.swf') }}"; var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/socket.io/WebSocketMain.swf') }}";
var WEB_SOCKET_DEBUG = true; var WEB_SOCKET_DEBUG = true;

View File

@ -44,6 +44,9 @@ class UserManager(object):
def getAllUsers(self): def getAllUsers(self):
return [] return []
def hasBeenCustomized(self):
return False
##~~ FilebasedUserManager, takes available users from users.yaml file ##~~ FilebasedUserManager, takes available users from users.yaml file
class FilebasedUserManager(UserManager): class FilebasedUserManager(UserManager):
@ -57,17 +60,19 @@ class FilebasedUserManager(UserManager):
self._users = {} self._users = {}
self._dirty = False self._dirty = False
self._customized = None
self._load() self._load()
def _load(self): def _load(self):
if os.path.exists(self._userfile) and os.path.isfile(self._userfile): if os.path.exists(self._userfile) and os.path.isfile(self._userfile):
self._customized = True
with open(self._userfile, "r") as f: with open(self._userfile, "r") as f:
data = yaml.safe_load(f) data = yaml.safe_load(f)
for name in data.keys(): for name in data.keys():
attributes = data[name] attributes = data[name]
self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"]) self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"])
else: else:
self._users["admin"] = User("admin", "7557160613d5258f883014a7c3c0428de53040fc152b1791f1cc04a62b428c0c2a9c46ed330cdce9689353ab7a5352ba2b2ceb459b96e9c8ed7d0cb0b2c0c076", True, ["user", "admin"]) self._customized = False
def _save(self, force=False): def _save(self, force=False):
if not self._dirty and not force: if not self._dirty and not force:
@ -169,6 +174,9 @@ class FilebasedUserManager(UserManager):
def getAllUsers(self): def getAllUsers(self):
return map(lambda x: x.asDict(), self._users.values()) return map(lambda x: x.asDict(), self._users.values())
def hasBeenCustomized(self):
return self._customized
##~~ Exceptions ##~~ Exceptions
class UserAlreadyExists(Exception): class UserAlreadyExists(Exception):

6
run
View File

@ -39,6 +39,10 @@ def main():
help="Daemonize/control daemonized OctoPrint instance (only supported under Linux right now)") help="Daemonize/control daemonized OctoPrint instance (only supported under Linux right now)")
parser.add_argument("--pid", action="store", type=str, dest="pidfile", default="/tmp/octoprint.pid", parser.add_argument("--pid", action="store", type=str, dest="pidfile", default="/tmp/octoprint.pid",
help="Pidfile to use for daemonizing, defaults to /tmp/octoprint.pid") help="Pidfile to use for daemonizing, defaults to /tmp/octoprint.pid")
parser.add_argument("--iknowwhatimdoing", action="store_true", dest="allowRoot",
help="Allow OctoPrint to run as user root")
args = parser.parse_args() args = parser.parse_args()
if args.daemon: if args.daemon:
@ -54,7 +58,7 @@ def main():
elif "restart" == args.daemon: elif "restart" == args.daemon:
daemon.restart() daemon.restart()
else: else:
octoprint = Server(args.config, args.basedir, args.host, args.port, args.debug) octoprint = Server(args.config, args.basedir, args.host, args.port, args.debug, args.allowRoot)
octoprint.run() octoprint.run()
if __name__ == "__main__": if __name__ == "__main__":