From f1e764f2e5728113f191456236d02fdae6e7680a Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 12 Jul 2012 17:21:54 -0400 Subject: [PATCH 01/24] integrate exmachina configuration management layer - add exmachina code and test code - modify plinth.py to listen for shared secret on stdin at start (if appropriate flag is set) and try to connect to exmachina daemon - use exmachina to read and set /etc/hostname as a demo - update plinth init.d script to start exmachina and share keys - update docs with new deps and run instructions --- INSTALL | 23 +- NOTES | 12 + README | 3 +- exmachina/__init__.py | 0 exmachina/exmachina.py | 353 +++++++++++++++++++++++++++++ exmachina/init_test.sh | 13 ++ exmachina/test_exmachina.py | 71 ++++++ fabfile.py | 2 +- modules/installed/system/config.py | 28 ++- plinth.py | 18 ++ share/init.d/plinth | 45 +++- 11 files changed, 544 insertions(+), 24 deletions(-) create mode 100644 exmachina/__init__.py create mode 100755 exmachina/exmachina.py create mode 100755 exmachina/init_test.sh create mode 100755 exmachina/test_exmachina.py diff --git a/INSTALL b/INSTALL index 9b2705496..9108689e2 100644 --- a/INSTALL +++ b/INSTALL @@ -2,10 +2,10 @@ ## Installing Plinth -Install the python-cheetah package and pandoc: - -apt-get install python-cheetah pandoc +Install the python-cheetah package, pandoc, python-augeas, and +bjsonrpc: +apt-get install python-cheetah pandoc python-augeas python-bjsonrpc Install the python-simplejson @@ -13,9 +13,16 @@ apt-get install python-simplejson Unzip and untar the source into a directory. Change to the directory -containing the program. Do `make` and then run `./plinth.py` and -point your web browser at `localhost:8000`. The default username is -"admin" and the default password is "secret". +containing the program. Run: + + $ make + $ ./plinth.py + +and point your web browser at `localhost:8000`. The default username is "admin" +and the default password is "secret". To actually edit the configuration of +your local/dev machine, also run: + + $ sudo ./exmachina/exmachina.py -v & ## Dependencies @@ -27,6 +34,10 @@ point your web browser at `localhost:8000`. The default username is * *GNU Make* is used to build the templates and such. +* bjsonrpc - used for configuration management layer + +* python-augeas and augeas - used for configuration management + The documentation has some dependencies too. * *Markdown* is used to format and style docs. diff --git a/NOTES b/NOTES index ff46c2c7d..dbdebd53c 100644 --- a/NOTES +++ b/NOTES @@ -2,6 +2,18 @@ % % February 2012 +# Edits by bnewbold + +## 2012-07-12 "exmachina" configuration management layer + +- this new code is very ugly and in the "just make it work" style +- add exmachina code and test code +- modify plinth.py to listen for shared secret on stdin at start + (if appropriate flag is set) and try to connect to exmachina daemon +- use exmachina to read and set /etc/hostname as a demo +- update plinth init.d script to start exmachina and share keys +- update docs with new deps and run instructions + # Edits by seandiggity ## 2012-02-27 new theme based upon bootstrap diff --git a/README b/README index 6d56c72fd..d2cdd3c1b 100644 --- a/README +++ b/README @@ -50,4 +50,5 @@ interface will overwrite those changes at first opportunity. This interface is not a tool for super admins facing complex scenarios. It is for home users to do a wide variety of basic tasks. - +See comments in exmachina/exmachina.py for more details about the configuration +management process seperation scheme. diff --git a/exmachina/__init__.py b/exmachina/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/exmachina/exmachina.py b/exmachina/exmachina.py new file mode 100755 index 000000000..fc04beb1d --- /dev/null +++ b/exmachina/exmachina.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python + +""" +Author: bnewbold +Date: July 2012 +License: GPLv3 (see http://www.gnu.org/licenses/gpl-3.0.html) + (two helper functions copied from web, as cited below) +Package Requirements: python-augeas, bjsonrpc + +This file implements both ends (privilaged daemon and unprivilaged python +client library) of a crude system configuration message bus, intended for use +(initially) with the Plinth web interface to the FreedomBox operating system. + +The goal is to provide partially-untrusted processes (such as the web interface +running as the www-data user) access to core system configuration files +(through the Augeas library) and daemon control (through the init.d scripts). + +The daemon process (started in the same startup script as Plinth) runs as root +and accepts JSON-RPC method calls through a unix domain socket +(/tmp/exmachina.sock by default). Because file access control may not be +sufficiently flexible for access control, a somewhat-elaborate secret key +mechanism can be used to control access to the RPC mechanism. + +The (optional) shared secret-key mechanism requires clients to first call the +"authenticate" RPC method before any other methods. The secret key is passed to +the server process through stdin at startup (command line arguments could be +snooped by unprivilaged processes), and would presumably be passed on to the +client in the same way. The init_test.sh script demonstrates this mechanism. +""" + +import os +import sys +import optparse +import logging +import socket +import subprocess +import stat +import time +import base64 + +import bjsonrpc +import bjsonrpc.handlers +import bjsonrpc.server +import augeas + + +log = logging.getLogger(__name__) + +def execute_service(servicename, action, timeout=10): + """This function mostly ripped from StackOverflow: + http://stackoverflow.com/questions/1556348/python-run-a-process-with-timeout-and-capture-stdout-stderr-and-exit-status + """ + # ensure service name isn't tricky trick + script = "/etc/init.d/" + os.path.split(servicename)[1] + + if not os.path.exists(script): + return "ERROR: so such service" + + command_list = [script, action] + log.info("executing: %s" % command_list) + proc = subprocess.Popen(command_list, + bufsize=0, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + poll_seconds = .250 + deadline = time.time() + timeout + while time.time() < deadline and proc.poll() == None: + time.sleep(poll_seconds) + + if proc.poll() == None: + if float(sys.version[:3]) >= 2.6: + proc.terminate() + raise Exception("execution timed out (>%d seconds): %s" % + (timeout, command_list)) + + stdout, stderr = proc.communicate() + return stdout, stderr, proc.returncode + +class ExMachinaHandler(bjsonrpc.handlers.BaseHandler): + + # authentication state variable. If not None, still need to authenticate; + # if None then authentication not require or was already successful for + # this instantiation of the Handler. This class variable gets optionally + # overridden on a per-process basis + secret_key = None + + def _setup(self): + self.augeas = augeas.Augeas() + + def authenticate(self, secret_key): + if not self.secret_key: + log.warn("Unecessary authentication attempt") + return + if not secret_key.strip() == self.secret_key.strip(): + # fail hard + log.error("Authentication failed!") + sys.exit() + self.secret_key = None + + # ------------- Augeas API Passthrough ----------------- + def augeas_save(self): + if not self.secret_key: + log.info("augeas: saving config") + return self.augeas.save() + + def augeas_set(self, path, value): + if not self.secret_key: + log.info("augeas: set %s=%s" % (path, value)) + return self.augeas.set(path.encode('utf-8'), + value.encode('utf-8')) + + def augeas_setm(self, base, sub, value): + if not self.secret_key: + log.info("augeas: setm %s %s = %s" % (base, sub, value)) + return self.augeas.setm(base.encode('utf-8'), + sub.encode('utf-8'), + value.encode('utf-8')) + + def augeas_get(self, path): + if not self.secret_key: + # reduce verbosity + log.debug("augeas: get %s" % path) + return self.augeas.get(path.encode('utf-8')) + + def augeas_match(self, path): + if not self.secret_key: + # reduce verbosity + log.debug("augeas: match %s" % path) + return self.augeas.match("%s" % path.encode('utf-8')) + + def augeas_insert(self, path, label, before=True): + if not self.secret_key: + log.info("augeas: insert %s=%s" % (path, value)) + return self.augeas.insert(path.encode('utf-8'), + label.encode('utf-8'), + before=before) + + def augeas_move(self, src, dst): + if not self.secret_key: + log.info("augeas: move %s -> %s" % (src, dst)) + return self.augeas.move(src.encode('utf-8'), dst.encode('utf-8')) + + def augeas_remove(self, path): + if not self.secret_key: + log.info("augeas: remove %s" % path) + return self.augeas.remove(path.encode('utf-8')) + + # ------------- init.d Service Control ----------------- + def initd_status(self, servicename): + if not self.secret_key: + return execute_service(servicename, "status") + + def initd_start(self, servicename): + if not self.secret_key: + return execute_service(servicename, "start") + + def initd_stop(self, servicename): + if not self.secret_key: + return execute_service(servicename, "stop") + + def initd_restart(self, servicename): + if not self.secret_key: + return execute_service(servicename, "restart") + +class EmptyClass(): + # Used by ExMachinaClient below + pass + +class ExMachinaClient(): + """Simple client wrapper library to expose augeas and init.d methods. + + In brief, use augeas.get/set/insert to modify system configuration files + under the /files/etc/* namespace. augeas.match with a wildcard can be used + to find variables to edit. + + After making any changes, use augeas.save to commit to disk, then + initd.restart to restart the appropriate system daemons. In many cases, + this would be the 'networking' meta-daemon. + + See test_exmachina.py for some simple examples; see the augeas docs for + more in depth guidance. + """ + + def __init__(self, + socket_path="/tmp/exmachina.sock", + secret_key=None): + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.connect(socket_path) + self.conn = bjsonrpc.connection.Connection(self.sock) + + if secret_key: + self.conn.call.authenticate(secret_key) + + self.augeas = EmptyClass() + self.initd = EmptyClass() + + self.augeas.save = self.conn.call.augeas_save + self.augeas.set = self.conn.call.augeas_set + self.augeas.setm = self.conn.call.augeas_setm + self.augeas.get = self.conn.call.augeas_get + self.augeas.match = self.conn.call.augeas_match + self.augeas.insert = self.conn.call.augeas_insert + self.augeas.move = self.conn.call.augeas_move + self.augeas.remove = self.conn.call.augeas_remove + self.initd.status = self.conn.call.initd_status + self.initd.start = self.conn.call.initd_start + self.initd.stop = self.conn.call.initd_stop + self.initd.restart = self.conn.call.initd_restart + + def close(self): + self.sock.close() + +def run_server(socket_path, secret_key=None): + + if not 0 == os.geteuid(): + log.warn("Expected to be running as root!") + + if os.path.exists(socket_path): + os.unlink(socket_path) + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.bind(socket_path) + sock.listen(1) + + # TODO: www-data group permissions only? + os.chmod(socket_path, 0666) + if secret_key: + ExMachinaHandler.secret_key = secret_key + + serv = bjsonrpc.server.Server(sock, handler_factory=ExMachinaHandler) + serv.serve() + +def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): + """ + From: http://www.noah.org/wiki/Daemonize_Python + + This forks the current process into a daemon. The stdin, stdout, and + stderr arguments are file names that will be opened and be used to replace + the standard file descriptors in sys.stdin, sys.stdout, and sys.stderr. + These arguments are optional and default to /dev/null. Note that stderr is + opened unbuffered, so if it shares a file with stdout then interleaved + output may not appear in the order that you expect. """ + + # Do first fork. + try: + pid = os.fork() + if pid > 0: + sys.exit(0) # Exit first parent. + except OSError, e: + sys.stderr.write ("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror) ) + sys.exit(1) + + # Decouple from parent environment. + os.chdir("/") + os.umask(0) + os.setsid() + + # Do second fork. + try: + pid = os.fork() + if pid > 0: + sys.exit(0) # Exit second parent. + except OSError, e: + sys.stderr.write ("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror) ) + sys.exit(1) + + # Now I am a daemon! + + # Redirect standard file descriptors. + si = open(stdin, 'r') + so = open(stdout, 'a+') + se = open(stderr, 'a+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + return pid + +# ============================================================================= +# Command line handling +def main(): + + global log + parser = optparse.OptionParser(usage= + "usage: %prog [options]\n" + "%prog --help for more info." + ) + parser.add_option("-v", "--verbose", + default=False, + help="Show more debugging statements", + action="store_true") + parser.add_option("-q", "--quiet", + default=False, + help="Show fewer informational statements", + action="store_true") + parser.add_option("-k", "--key", + default=False, + help="Wait for Secret Access Key on stdin before starting", + action="store_true") + parser.add_option("--random-key", + default=False, + help="Just dump a random base64 key and exit", + action="store_true") + parser.add_option("-s", "--socket-path", + default="/tmp/exmachina.sock", + help="UNIX Domain socket file path to listen on", + metavar="FILE") + parser.add_option("--pidfile", + default=None, + help="Daemonize and write pid to this file", + metavar="FILE") + + (options, args) = parser.parse_args() + + if len(args) != 0: + parser.error("Incorrect number of arguments") + + if options.random_key: + sys.stdout.write(base64.urlsafe_b64encode(os.urandom(128))) + sys.exit(0) + + log = logging.getLogger() + hdlr = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + hdlr.setFormatter(formatter) + log.addHandler(hdlr) + + if options.verbose: + log.setLevel(logging.DEBUG) + elif options.quiet: + log.setLevel(logging.ERROR) + else: + log.setLevel(logging.INFO) + + secret_key = None + if options.key: + log.debug("Waiting for secret key on stdin...") + secret_key = sys.stdin.readline().strip() + log.debug("Got it!") + + if options.pidfile: + with open(options.pidfile, 'w') as pfile: + # ensure file is available/writable + pass + os.unlink(options.pidfile) + daemonize() + pid = os.getpid() + with open(options.pidfile, 'w') as pfile: + pfile.write("%s" % pid) + log.info("Daemonized, pid is %s" % pid) + + run_server(secret_key=secret_key, socket_path=options.socket_path) + +if __name__ == '__main__': + main() diff --git a/exmachina/init_test.sh b/exmachina/init_test.sh new file mode 100755 index 000000000..c53d76bd2 --- /dev/null +++ b/exmachina/init_test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Test init.d-style initialization; run this script as root (or sudo it) + +export key=`./exmachina.py --random-key` + +echo $key | ./exmachina.py -vk --pidfile /tmp/exmachina_test.pid +sleep 1 +echo $key | sudo -u www-data -g www-data ./test_exmachina.py -k + +kill `cat /tmp/exmachina_test.pid` && rm /tmp/exmachina_test.pid +sleep 1 +jobs diff --git a/exmachina/test_exmachina.py b/exmachina/test_exmachina.py new file mode 100755 index 000000000..86c71bf9b --- /dev/null +++ b/exmachina/test_exmachina.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +""" +This file tests the "client side" of the exmachina layer. + +To use with secret keys, do the following in seperate terminals: + + $ echo "" | ./exmachina.py -vk + $ echo "" | ./test.py -k + +To use without, do the following in seperate terminals: + + $ echo "" | ./exmachina.py -vk + $ echo "" | ./test.py -k + +Use the init_test.sh script to test shared key passing and privilage seperation +at the same time: + + $ sudo ./init_test.sh +""" + +import sys +import optparse +import logging +import socket + +import bjsonrpc +import bjsonrpc.connection +import augeas + +from exmachina import ExMachinaClient + +# ============================================================================= +# Command line handling +def main(): + + socket_path="/tmp/exmachina.sock" + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(socket_path) + + secret_key = None + if sys.argv[-1] == "-k": + print "waiting for key on stdin..." + secret_key = sys.stdin.readline() + print "sent!" + + print "========= Testing JSON-RPC connection" + c = bjsonrpc.connection.Connection(sock) + if secret_key: + c.call.authenticate(secret_key) + print "/*: %s" % c.call.augeas_match("/*") + print "/augeas/*: %s" % c.call.augeas_match("/augeas/*") + print "/etc/* files:" + for name in c.call.augeas_match("/files/etc/*"): + print "\t%s" % name + print c.call.initd_status("bluetooth") + print "hostname: %s" % c.call.augeas_get("/files/etc/hostname/*") + print "localhost: %s" % c.call.augeas_get("/files/etc/hosts/1/canonical") + sock.close() + + print "========= Testing user client library" + client = ExMachinaClient(secret_key=secret_key) + print client.augeas.match("/files/etc/*") + #print client.initd.restart("bluetooth") + print client.initd.status("greentooth") + print "(expect Error on the above line)" + print client.initd.status("bluetooth") + client.close() + +if __name__ == '__main__': + main() diff --git a/fabfile.py b/fabfile.py index 92a52e196..4b4d25410 100644 --- a/fabfile.py +++ b/fabfile.py @@ -138,7 +138,7 @@ def apache(): @task def deps(): "Basic plinth dependencies" - sudo('apt-get install --no-install-recommends -y python make python-cheetah pandoc python-simplejson python-pyme') + sudo('apt-get install --no-install-recommends -y python make python-cheetah pandoc python-simplejson python-pyme python-augeas python-bjsonrpc') @task def update(): diff --git a/modules/installed/system/config.py b/modules/installed/system/config.py index c8b5190a8..b31dc6009 100644 --- a/modules/installed/system/config.py +++ b/modules/installed/system/config.py @@ -41,20 +41,18 @@ def valid_hostname(name): def set_hostname(hostname): "Sets machine hostname to hostname" - cfg.log.info("Writing '%s' to /etc/hostname" % hostname) - unslurp("/etc/hostname", hostname+"\n") + cfg.log.info("Writing '%s' to /etc/hostname with exmachina" % hostname) + try: - retcode = subprocess.call("/etc/init.d/hostname.sh start", shell=True) - if retcode < 0: - cfg.log.error("Hostname restart terminated by signal: return code is %s" % retcode) - else: - cfg.log.debug("Hostname restart returned %s" % retcode) + cfg.exmachina.augeas.set("/files/etc/hostname/*", hostname) + cfg.exmachina.augeas.save() + # don't persist/cache change unless it was saved successfuly + sys_store = filedict_con(cfg.store_file, 'sys') + sys_store['hostname'] = hostname + cfg.exmachina.initd.restart("hostname.sh") # is hostname.sh debian-only? except OSError, e: raise cherrypy.HTTPError(500, "Hostname restart failed: %s" % e) - sys_store = filedict_con(cfg.store_file, 'sys') - sys_store['hostname'] = hostname - class general(FormPlugin, PagePlugin): url = ["/sys/config"] order = 30 @@ -72,8 +70,11 @@ class general(FormPlugin, PagePlugin): def main(self, message='', **kwargs): sys_store = filedict_con(cfg.store_file, 'sys') + hostname = cfg.exmachina.augeas.get("/files/etc/hostname/*") + # this layer of persisting configuration in sys_store could/should be + # removed -BLN defaults = {'time_zone': "slurp('/etc/timezone').rstrip()", - 'hostname': "gethostname()", + 'hostname': "hostname", } for k,c in defaults.items(): if not k in kwargs: @@ -81,6 +82,8 @@ class general(FormPlugin, PagePlugin): kwargs[k] = sys_store[k] except KeyError: exec("if not '%(k)s' in kwargs: sys_store['%(k)s'] = kwargs['%(k)s'] = %(c)s" % {'k':k, 'c':c}) + # over-ride the sys_store cached value + kwargs['hostname'] = hostname ## Get the list of supported timezones and the index in that list of the current one module_file = __file__ @@ -120,7 +123,8 @@ class general(FormPlugin, PagePlugin): old_val = sys_store['hostname'] try: set_hostname(hostname) - except: + except Exception, e: + cfg.log.error(e) cfg.log.info("Trying to restore old hostname value.") set_hostname(old_val) raise diff --git a/plinth.py b/plinth.py index 2eff29f40..adab11012 100755 --- a/plinth.py +++ b/plinth.py @@ -17,6 +17,9 @@ from util import * from logger import Logger #from modules.auth import AuthController, require, member_of, name_is +from exmachina.exmachina import ExMachinaClient +import socket + __version__ = "0.2.14" __author__ = "James Vasile" __copyright__ = "Copyright 2011, James Vasile" @@ -71,9 +74,17 @@ def parse_arguments(): parser = argparse.ArgumentParser(description='Plinth web interface for the FreedomBox.') parser.add_argument('--pidfile', default="", help='specify a file in which the server may write its pid') + parser.add_argument('--listen-exmachina-key', default=False, action='store_true', + help='listen for JSON-RPC shared secret key on stdin at startup') args=parser.parse_args() if args.pidfile: cfg.pidfile = args.pidfile + if args.listen_exmachina_key: + # this is where we optionally try to read in a shared secret key to + # authenticate connections to exmachina + cfg.exmachina_secret_key = sys.stdin.readline().strip() + else: + cfg.exmachina_secret_key = None def setup(): parse_arguments() @@ -85,6 +96,13 @@ def setup(): except AttributeError: pass + try: + cfg.exmachina = ExMachinaClient( + secret_key=cfg.exmachina_secret_key or None) + except socket.error: + cfg.exmachina = None + print "couldn't connect to exmachina daemon, but continuing anyways..." + os.chdir(cfg.file_root) cherrypy.config.update({'error_page.404': error_page_404}) cherrypy.config.update({'error_page.500': error_page_500}) diff --git a/share/init.d/plinth b/share/init.d/plinth index e364fc123..8cf88a93c 100755 --- a/share/init.d/plinth +++ b/share/init.d/plinth @@ -12,23 +12,53 @@ # This file is /etc/init.d/plinth DAEMON=/usr/local/bin/plinth.py +EXMACHINA_DAEMON=/usr/local/bin/exmachina.py PID_FILE=/var/run/plinth.pid - +EXMACHINA_PID_FILE=/var/run/exmachina.pid + +PLINTH_USER=www-data +PLINTH_GROUP=www-data + +test -x $DAEMON || exit 0 +test -x $EXMACHINA_DAEMON || exit 0 + +set -e + +. /lib/lsb/init-functions + start_plinth (){ if [ -f $PID_FILE ]; then - echo Already running with a pid of `cat $PID_FILE`. + echo Already running with a pid of `cat $PID_FILE`. else - $DAEMON --pidfile=$PID_FILE + if [ -f $EXMACHINA_PID_FILE ]; then + echo exmachina was already running with a pid of `cat $EXMACHINA_PID_FILE`. + kill -15 `cat $EXMACHINA_PID_FILE` + rm -rf $EXMACHINA_PID_FILE + fi + SHAREDKEY=`$EXMACHINA_DAEMON --random-key` + touch $PID_FILE + chown $PLINTH_USER:$PLINTH_GROUP $PID_FILE + echo $SHAREDKEY | $EXMACHINA_DAEMON --pidfile=$EXMACHINA_PID_FILE || rm $PID_FILE + sleep 0.5 + echo $SHAREDKEY | sudo -u $PLINTH_USER -g $PLINTH_GROUP $DAEMON --pidfile=$PID_FILE fi } stop_plinth () { if [ -f $PID_FILE ]; then - kill -15 `cat $PID_FILE` + kill -15 `cat $PID_FILE` || true rm -rf $PID_FILE + echo "killed plinth" else echo "No pid file at $PID_FILE suggests plinth is not running." fi + if [ -f $EXMACHINA_PID_FILE ]; then + kill -15 `cat $EXMACHINA_PID_FILE` || true + rm -rf $EXMACHINA_PID_FILE + echo "killed exmachina" + else + echo "No pid file at $EXMACHINA_PID_FILE suggests exmachina is not running." + fi } test -x $DAEMON || exit 0 @@ -45,5 +75,12 @@ case "$1" in $0 stop $0 start ;; + status) + status_of_proc -p $PID_FILE "$DAEMON" plinth && exit 0 || exit $? + ;; + *) + echo "Usage: $NAME {start|stop|restart|status}" >&2 + exit 1 + ;; esac From 0430892518760d139258dcf8f8de0045bee4b2e3 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Wed, 25 Jul 2012 23:14:42 -0700 Subject: [PATCH 02/24] updates/improvements to exmachina --- exmachina/exmachina.py | 174 ++++++++++++++++++++++++++---------- exmachina/init_test.sh | 2 +- exmachina/test_exmachina.py | 21 +++-- 3 files changed, 146 insertions(+), 51 deletions(-) diff --git a/exmachina/exmachina.py b/exmachina/exmachina.py index fc04beb1d..21de49322 100755 --- a/exmachina/exmachina.py +++ b/exmachina/exmachina.py @@ -30,11 +30,12 @@ client in the same way. The init_test.sh script demonstrates this mechanism. import os import sys -import optparse +import grp +import shutil +import argparse import logging import socket import subprocess -import stat import time import base64 @@ -43,9 +44,9 @@ import bjsonrpc.handlers import bjsonrpc.server import augeas - log = logging.getLogger(__name__) + def execute_service(servicename, action, timeout=10): """This function mostly ripped from StackOverflow: http://stackoverflow.com/questions/1556348/python-run-a-process-with-timeout-and-capture-stdout-stderr-and-exit-status @@ -54,7 +55,7 @@ def execute_service(servicename, action, timeout=10): script = "/etc/init.d/" + os.path.split(servicename)[1] if not os.path.exists(script): - return "ERROR: so such service" + raise ValueError("so such service: %s" % servicename) command_list = [script, action] log.info("executing: %s" % command_list) @@ -64,18 +65,51 @@ def execute_service(servicename, action, timeout=10): stderr=subprocess.PIPE) poll_seconds = .250 deadline = time.time() + timeout - while time.time() < deadline and proc.poll() == None: + while time.time() < deadline and proc.poll() is None: time.sleep(poll_seconds) - if proc.poll() == None: + if proc.poll() is None: if float(sys.version[:3]) >= 2.6: proc.terminate() - raise Exception("execution timed out (>%d seconds): %s" % + raise Exception("execution timed out (>%d seconds): %s" % + (timeout, command_list)) + + stdout, stderr = proc.communicate() + # TODO: should raise exception here if proc.returncode != 0? + return stdout, stderr, proc.returncode + +def execute_apt(packagename, action, timeout=120, aptargs=['-q', '-y']): + # ensure package name isn't tricky trick + if action != "update" \ + and (packagename != packagename.strip().split()[0] \ + or packagename.startswith('-')): + raise ValueError("Not a good apt package name: %s" % packagename) + + if action == "update": + command_list = ['apt-get', action] + else: + command_list = ['apt-get', action, packagename] + command_list.extend(aptargs) + log.info("executing: %s" % command_list) + proc = subprocess.Popen(command_list, + bufsize=0, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + poll_seconds = .250 + deadline = time.time() + timeout + while time.time() < deadline and proc.poll() is None: + time.sleep(poll_seconds) + + if proc.poll() is None: + if float(sys.version[:3]) >= 2.6: + proc.terminate() + raise Exception("execution timed out (>%d seconds): %s" % (timeout, command_list)) stdout, stderr = proc.communicate() return stdout, stderr, proc.returncode + class ExMachinaHandler(bjsonrpc.handlers.BaseHandler): # authentication state variable. If not None, still need to authenticate; @@ -145,6 +179,24 @@ class ExMachinaHandler(bjsonrpc.handlers.BaseHandler): log.info("augeas: remove %s" % path) return self.augeas.remove(path.encode('utf-8')) + # ------------- Misc. non-Augeas Helpers ----------------- + def set_timezone(self, tzname): + if not self.secret_key: + log.info("reset timezone to %s" % tzname) + tzname = tzname.strip() + tzpath = os.path.join("/usr/share/zoneinfo", tzname) + try: + os.stat(tzpath) + except OSError: + # file not found + raise ValueError("timezone not valid: %s" % tzname) + shutil.copy( + os.path.join("/usr/share/zoneinfo", tzname), + "/etc/localtime") + with open("/etc/timezone", "w") as tzfile: + tzfile.write(tzname + "\n") + return "timezone changed to %s" % tzname + # ------------- init.d Service Control ----------------- def initd_status(self, servicename): if not self.secret_key: @@ -161,11 +213,26 @@ class ExMachinaHandler(bjsonrpc.handlers.BaseHandler): def initd_restart(self, servicename): if not self.secret_key: return execute_service(servicename, "restart") - + + # ------------- apt-get Package Control ----------------- + def apt_install(self, packagename): + if not self.secret_key: + return execute_apt(packagename, "install") + + def apt_update(self): + if not self.secret_key: + return execute_apt("", "update") + + def apt_remove(self, packagename): + if not self.secret_key: + return execute_apt(packagename, "remove") + + class EmptyClass(): # Used by ExMachinaClient below pass + class ExMachinaClient(): """Simple client wrapper library to expose augeas and init.d methods. @@ -193,6 +260,8 @@ class ExMachinaClient(): self.augeas = EmptyClass() self.initd = EmptyClass() + self.apt = EmptyClass() + self.misc = EmptyClass() self.augeas.save = self.conn.call.augeas_save self.augeas.set = self.conn.call.augeas_set @@ -206,11 +275,16 @@ class ExMachinaClient(): self.initd.start = self.conn.call.initd_start self.initd.stop = self.conn.call.initd_stop self.initd.restart = self.conn.call.initd_restart + self.apt.install = self.conn.call.apt_install + self.apt.update = self.conn.call.apt_update + self.apt.remove = self.conn.call.apt_remove + self.misc.set_timezone = self.conn.call.set_timezone def close(self): self.sock.close() -def run_server(socket_path, secret_key=None): + +def run_server(socket_path, secret_key=None, socket_group=None): if not 0 == os.geteuid(): log.warn("Expected to be running as root!") @@ -221,15 +295,20 @@ def run_server(socket_path, secret_key=None): sock.bind(socket_path) sock.listen(1) - # TODO: www-data group permissions only? - os.chmod(socket_path, 0666) + if socket_group is not None: + socket_uid = os.stat(socket_path).st_uid + socket_gid = grp.getgrnam(socket_group).gr_gid + os.chmod(socket_path, 0660) + os.chown(socket_path, socket_uid, socket_gid) + else: + os.chmod(socket_path, 0666) if secret_key: ExMachinaHandler.secret_key = secret_key serv = bjsonrpc.server.Server(sock, handler_factory=ExMachinaHandler) serv.serve() -def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): +def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): """ From: http://www.noah.org/wiki/Daemonize_Python @@ -241,30 +320,30 @@ def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): output may not appear in the order that you expect. """ # Do first fork. - try: - pid = os.fork() + try: + pid = os.fork() if pid > 0: sys.exit(0) # Exit first parent. - except OSError, e: - sys.stderr.write ("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror) ) + except OSError, e: + sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror)) sys.exit(1) # Decouple from parent environment. - os.chdir("/") - os.umask(0) - os.setsid() + os.chdir("/") + os.umask(0) + os.setsid() # Do second fork. - try: - pid = os.fork() + try: + pid = os.fork() if pid > 0: sys.exit(0) # Exit second parent. - except OSError, e: - sys.stderr.write ("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror) ) + except OSError, e: + sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror)) sys.exit(1) # Now I am a daemon! - + # Redirect standard file descriptors. si = open(stdin, 'r') so = open(stdout, 'a+') @@ -279,41 +358,44 @@ def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): def main(): global log - parser = optparse.OptionParser(usage= + parser = argparse.ArgumentParser(usage= "usage: %prog [options]\n" "%prog --help for more info." ) - parser.add_option("-v", "--verbose", + parser.add_argument("-v", "--verbose", default=False, - help="Show more debugging statements", + help="Show more debugging statements", action="store_true") - parser.add_option("-q", "--quiet", + parser.add_argument("-q", "--quiet", default=False, - help="Show fewer informational statements", + help="Show fewer informational statements", action="store_true") - parser.add_option("-k", "--key", + parser.add_argument("-k", "--key", default=False, help="Wait for Secret Access Key on stdin before starting", action="store_true") - parser.add_option("--random-key", + parser.add_argument("--random-key", default=False, help="Just dump a random base64 key and exit", action="store_true") - parser.add_option("-s", "--socket-path", + parser.add_argument("-s", "--socket-path", default="/tmp/exmachina.sock", help="UNIX Domain socket file path to listen on", metavar="FILE") - parser.add_option("--pidfile", + parser.add_argument("--pidfile", default=None, help="Daemonize and write pid to this file", metavar="FILE") + parser.add_argument("-g", "--group", + default=None, + help="chgrp socket file to this group and set 0660 permissions") - (options, args) = parser.parse_args() + args = parser.parse_args() - if len(args) != 0: - parser.error("Incorrect number of arguments") + #if len(args) != 0: + #parser.error("Incorrect number of arguments") - if options.random_key: + if args.random_key: sys.stdout.write(base64.urlsafe_b64encode(os.urandom(128))) sys.exit(0) @@ -323,31 +405,33 @@ def main(): hdlr.setFormatter(formatter) log.addHandler(hdlr) - if options.verbose: + if args.verbose: log.setLevel(logging.DEBUG) - elif options.quiet: + elif args.quiet: log.setLevel(logging.ERROR) else: log.setLevel(logging.INFO) secret_key = None - if options.key: + if args.key: log.debug("Waiting for secret key on stdin...") secret_key = sys.stdin.readline().strip() log.debug("Got it!") - if options.pidfile: - with open(options.pidfile, 'w') as pfile: + if args.pidfile: + with open(args.pidfile, 'w') as pfile: # ensure file is available/writable pass - os.unlink(options.pidfile) + os.unlink(args.pidfile) daemonize() pid = os.getpid() - with open(options.pidfile, 'w') as pfile: + with open(args.pidfile, 'w') as pfile: pfile.write("%s" % pid) log.info("Daemonized, pid is %s" % pid) - run_server(secret_key=secret_key, socket_path=options.socket_path) + run_server(secret_key=secret_key, + socket_path=args.socket_path, + socket_group=args.group) if __name__ == '__main__': main() diff --git a/exmachina/init_test.sh b/exmachina/init_test.sh index c53d76bd2..941285d36 100755 --- a/exmachina/init_test.sh +++ b/exmachina/init_test.sh @@ -4,7 +4,7 @@ export key=`./exmachina.py --random-key` -echo $key | ./exmachina.py -vk --pidfile /tmp/exmachina_test.pid +echo $key | ./exmachina.py -vk --pidfile /tmp/exmachina_test.pid -g www-data sleep 1 echo $key | sudo -u www-data -g www-data ./test_exmachina.py -k diff --git a/exmachina/test_exmachina.py b/exmachina/test_exmachina.py index 86c71bf9b..e8d239d32 100755 --- a/exmachina/test_exmachina.py +++ b/exmachina/test_exmachina.py @@ -20,13 +20,11 @@ at the same time: """ import sys -import optparse -import logging import socket import bjsonrpc import bjsonrpc.connection -import augeas +from bjsonrpc.exceptions import ServerError from exmachina import ExMachinaClient @@ -34,7 +32,7 @@ from exmachina import ExMachinaClient # Command line handling def main(): - socket_path="/tmp/exmachina.sock" + socket_path = "/tmp/exmachina.sock" sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.connect(socket_path) @@ -62,9 +60,22 @@ def main(): client = ExMachinaClient(secret_key=secret_key) print client.augeas.match("/files/etc/*") #print client.initd.restart("bluetooth") - print client.initd.status("greentooth") + try: + print client.initd.status("greentooth") + print "ERROR: should have failed above!" + except ServerError: + print "(got expected error, good!)" print "(expect Error on the above line)" print client.initd.status("bluetooth") + print client.apt.install("pkg_which_does_not_exist") + print client.apt.remove("pkg_which_does_not_exist") + #print client.apt.update() # can be slow... + #print client.misc.set_timezone("UTC") # don't clobber system... + try: + print client.misc.set_timezone("whoopie") # should be an error + print "ERROR: should have failed above!" + except ServerError: + print "(got expected error, good!)" client.close() if __name__ == '__main__': From 53d1d8f2801d8142b28877a639c1d6920d5fb950 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 26 Jul 2012 12:30:29 -0700 Subject: [PATCH 03/24] add exmachina HOWTO --- exmachina/HOWTO | 59 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 exmachina/HOWTO diff --git a/exmachina/HOWTO b/exmachina/HOWTO new file mode 100644 index 000000000..f76b3448f --- /dev/null +++ b/exmachina/HOWTO @@ -0,0 +1,59 @@ + +HOWTO: Use exmachina in Plinth to Change Configuration and Restart Services +----------------------------------------------------------------------------- + +exmachina is the name of a small and simple privilege separation layer that +lets Plinth (running as a web application with user/group www-data) edit +FreedomBox system configuration (which requires root privileges). The Augeas +library is used to provide a uniform interface to *most* configuration files in +/etc/, and a wrapper around init.d scripts allows services to be restarted +(which is often the way to have configuration changes go live). + +The exmachina daemon and client library is initialized when Plinth starts, and +is accessible in Plinth code through the "cfg" global variable: + + import cfg + cfg.exmachina.initd.restart("networking") + +Existing on-disk configuration settings can be read back through the "get" +action, and files/settings can be found with the "match" method. For example, +the following would print out a list of all the files in /etc/ with +augeas-editing support, and then print the system's hostname according to +/etc/hostname (a minimal one-line configuration file): + + for fname in c.call.augeas_match("/files/etc/*"): + print fname + cfg.exmachina.augeas.get("/files/etc/hostname/*") + +The mutating Augeas actions ("set", "insert", "remove", etc) operate on an +in-memory cache of the configuration files; to commit all changes (for all +modified files) to disk, use the "save" action. The following would set the +local hostname to fluffy.example.net, and then commit that change to disk. + + cfg.exmachina.augeas.set("/files/etc/hostname/*", "fluffy.example.net") + cfg.exmachina.augeas.save() + +After the above, the /etc/hostname file will be updated, but running network +services will not have picked up on the change. To have changes that have been +written to disk actually take effect, the "initd" module allows access to +restarting system services. In the case of hostnames, in debian there exists a +helper script called "/etc/init.d/hostname.sh", so the following would be used +to update all network daemons to a new hostname: + + cfg.exmachina.initd.restart("hostname.sh") + +Augeas makes some effort to prevent partial writes or configuration file +corruption, and in some cases provides basic type checking. However, it does +not do any logical error detection or correction. For example, two separate +services could be configured to try and listen on the same port, or an invalid +hostname could be specified (eg, "&.^$#"). Application code in Plinth is +responsible for ensuring the logical correctness of configuration changes. + +If there is a serious problem executing an exmachina method call (eg, tried to +restart a non-existent service), a bjsonrpc.exceptions.ServerError exception +will be raised, with the actual exception message (but not the traceback) +passed through. + +Reading: +- Augeas API notes: http://augeas.net/docs/api.html +- exmachina repository: https://github.com/bnewbold/exmachina From 09fbb23e90b19bf831888644f94ed3ca7e984bb7 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 26 Jul 2012 12:41:26 -0700 Subject: [PATCH 04/24] use exmachina to configure timezone --- modules/installed/system/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/installed/system/config.py b/modules/installed/system/config.py index b31dc6009..01c4ec693 100644 --- a/modules/installed/system/config.py +++ b/modules/installed/system/config.py @@ -1,4 +1,4 @@ -import os, shutil, subprocess +import os, subprocess from socket import gethostname import cherrypy import simplejson as json @@ -132,8 +132,8 @@ class general(FormPlugin, PagePlugin): message += msg if time_zone != sys_store['time_zone']: src = os.path.join("/usr/share/zoneinfo", time_zone) - cfg.log.info("Copying %s to /etc/localtime" % src) - shutil.copy(src, "/etc/localtime") + cfg.log.info("Setting timezone to %s" % time_zone) + cfg.exmachina.misc.set_timezone(time_zone) sys_store['time_zone'] = time_zone return message or "Settings updated." From 9519ac9ebfc8954ccbfa4d0822e478460a25d2e1 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 26 Jul 2012 12:42:06 -0700 Subject: [PATCH 05/24] use permissions for exmachina socket --- share/init.d/plinth | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/init.d/plinth b/share/init.d/plinth index 8cf88a93c..90bfcec98 100755 --- a/share/init.d/plinth +++ b/share/init.d/plinth @@ -38,7 +38,7 @@ start_plinth (){ SHAREDKEY=`$EXMACHINA_DAEMON --random-key` touch $PID_FILE chown $PLINTH_USER:$PLINTH_GROUP $PID_FILE - echo $SHAREDKEY | $EXMACHINA_DAEMON --pidfile=$EXMACHINA_PID_FILE || rm $PID_FILE + echo $SHAREDKEY | $EXMACHINA_DAEMON --pidfile=$EXMACHINA_PID_FILE --group=$PLINTH_GROUP || rm $PID_FILE sleep 0.5 echo $SHAREDKEY | sudo -u $PLINTH_USER -g $PLINTH_GROUP $DAEMON --pidfile=$PID_FILE fi From 7c9cb6ccb9f735e78ea82230d5488c9efbe2e592 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 26 Jul 2012 12:53:42 -0700 Subject: [PATCH 06/24] update .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 2088e658a..570bf0b09 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ templates/*.py TODO \#* .#* +cfg.py +cherrypy.config +data/users.sqlite3 + From 82df0a800d3894bb35eae3d53a57b508b4b4803f Mon Sep 17 00:00:00 2001 From: bnewbold Date: Tue, 7 Aug 2012 16:42:29 -0700 Subject: [PATCH 07/24] fix test comments (thx nick daly) --- exmachina/test_exmachina.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exmachina/test_exmachina.py b/exmachina/test_exmachina.py index e8d239d32..6c1db9418 100755 --- a/exmachina/test_exmachina.py +++ b/exmachina/test_exmachina.py @@ -5,13 +5,13 @@ This file tests the "client side" of the exmachina layer. To use with secret keys, do the following in seperate terminals: - $ echo "" | ./exmachina.py -vk - $ echo "" | ./test.py -k + $ echo "" | sudo ./exmachina.py -vk + $ echo "" | ./test_exmachina.py -k To use without, do the following in seperate terminals: - $ echo "" | ./exmachina.py -vk - $ echo "" | ./test.py -k + $ sudo ./exmachina.py -v + $ ./test_exmachina.py Use the init_test.sh script to test shared key passing and privilage seperation at the same time: From 3ef8e8bbc25854a699b386d57497dd5c93f20c20 Mon Sep 17 00:00:00 2001 From: Nick Daly Date: Sun, 23 Sep 2012 18:32:16 -0500 Subject: [PATCH 08/24] Customize host in cfg.py. We start with a default of 127.0.0.1, because anything else is stupid. Change to 0.0.0.0 to serve to everybody. --- cfg.sample.py | 1 + plinth.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cfg.sample.py b/cfg.sample.py index 799984a86..ee57bb6e0 100644 --- a/cfg.sample.py +++ b/cfg.sample.py @@ -12,6 +12,7 @@ users_dir = os.path.join(data_dir, "users") product_name = "Plinth" box_name = "FreedomBox" +host = 127.0.0.1 port = 8000 ## Do not edit below this line ## diff --git a/plinth.py b/plinth.py index adab11012..5cdce88e2 100755 --- a/plinth.py +++ b/plinth.py @@ -54,7 +54,7 @@ class Root(plugin_mount.PagePlugin): raise cherrypy.InternalRedirect('/router') else: raise cherrypy.InternalRedirect('/help/about') - + def load_modules(): """Import all the symlinked .py files in the modules directory and all the .py files in directories linked in the modules directory @@ -121,7 +121,7 @@ def setup(): server.subscribe() # Configure default server - cherrypy.config.update({'server.socket_host': '127.0.0.1', + cherrypy.config.update({'server.socket_host': cfg.host, 'server.socket_port': cfg.port, 'server.thread_pool':10, 'tools.staticdir.root': cfg.file_root, @@ -130,7 +130,7 @@ def setup(): 'tools.sessions.storage_type':"file", 'tools.sessions.timeout':90, 'tools.sessions.storage_path':"%s/data/cherrypy_sessions" % cfg.file_root, - + }) config = {'/': {'tools.staticdir.root': '%s/static' % cfg.file_root, @@ -142,7 +142,7 @@ def setup(): } cherrypy.tree.mount(cfg.html_root, '/', config=config) cherrypy.engine.signal_handler.subscribe() - + def main(): setup() From 480aacfac55772128e18366aa3870d69bc636b0e Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Thu, 4 Oct 2012 21:39:12 -0400 Subject: [PATCH 09/24] Fix host cfg and print on startup --- cfg.sample.py | 2 +- plinth.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cfg.sample.py b/cfg.sample.py index ee57bb6e0..160272446 100644 --- a/cfg.sample.py +++ b/cfg.sample.py @@ -12,7 +12,7 @@ users_dir = os.path.join(data_dir, "users") product_name = "Plinth" box_name = "FreedomBox" -host = 127.0.0.1 +host = "127.0.0.1" port = 8000 ## Do not edit below this line ## diff --git a/plinth.py b/plinth.py index 5cdce88e2..28f7207b1 100755 --- a/plinth.py +++ b/plinth.py @@ -146,7 +146,7 @@ def setup(): def main(): setup() - print "localhost %d" % cfg.port + print "%s %d" % (cfg.host, cfg.port) cherrypy.engine.start() cherrypy.engine.block() From f03e184c78c2a78abd03e45a335262b945e24e12 Mon Sep 17 00:00:00 2001 From: Nick Daly Date: Sun, 7 Oct 2012 20:28:42 -0500 Subject: [PATCH 10/24] Added default pidfile. --- plinth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plinth.py b/plinth.py index 5cdce88e2..d2a3e3ed4 100755 --- a/plinth.py +++ b/plinth.py @@ -79,6 +79,8 @@ def parse_arguments(): args=parser.parse_args() if args.pidfile: cfg.pidfile = args.pidfile + else: + cfg.pidfile = "plinth.pid" if args.listen_exmachina_key: # this is where we optionally try to read in a shared secret key to # authenticate connections to exmachina From ded54c9c48bb8ff829ec628961c0517baaf89883 Mon Sep 17 00:00:00 2001 From: Nick Daly Date: Sun, 7 Oct 2012 20:36:50 -0500 Subject: [PATCH 11/24] Created link issue. Plinth really should link clients to the right pages. --- issues/links.org | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 issues/links.org diff --git a/issues/links.org b/issues/links.org new file mode 100644 index 000000000..c95b563c5 --- /dev/null +++ b/issues/links.org @@ -0,0 +1,42 @@ +# -*- mode: org; mode: auto-fill; fill-column: 80 -*- + +#+TITLE: Make Links Portable +#+OPTIONS: d:t +#+LINK_UP: ./ +#+LINK_HOME: ../ + +* Issue + + Currently, all the links in Plinth point to 127.0.0.1/(something), and that + sucks for serving Plinth on a local network, like most use cases imply. + +* Fixes [0/1] + +** TODO Links work when accessed from remote clients. + + Investigate the following: + + : grep -nHr basehref ../* + + : grep -nHr 127.0 ../* + + #+begin_ascii + fabfile.py:40: if env.host == "localhost" or env.host=="127.0.0.1": + fabfile.py:46: if env.host == "localhost" or env.host=="127.0.0.1": + fabfile.py:102: hidden_service_config = "HiddenServiceDir %s\nHiddenServicePort 80 127.0.0.1:%d" % (tor_dir, santiago_port) + modules/installed/santiago/santiago.py:48: hidden_service_config = "HiddenServiceDir %s\nHiddenServicePort 80 127.0.0.1:%d" % (self.tor_dir, santiago_port) + plinth.py:119: server.socket_host = '127.0.0.1' + #+end_ascii + + Also, why is base_href blank in [[file:~/programs/freedombox/plinth/cfg.sample.py][cfg.sample.py]]? + +* Discussion + +* Metadata + :PROPERTIES: + :Status: Incomplete + :Priority: 0 + :Owner: Nick Daly + :Description: + :Tags: + :END: From 88225aa3ae7b5178b24961f6cb005f40c55bdd7d Mon Sep 17 00:00:00 2001 From: Nick Daly Date: Sun, 11 Nov 2012 12:25:11 -0600 Subject: [PATCH 12/24] Created start.sh to handle ExMachina outside of Plinth. --- INSTALL | 20 +++++++++----------- start.sh | 8 ++++++++ 2 files changed, 17 insertions(+), 11 deletions(-) create mode 100755 start.sh diff --git a/INSTALL b/INSTALL index 9108689e2..e5761a149 100644 --- a/INSTALL +++ b/INSTALL @@ -11,18 +11,18 @@ Install the python-simplejson apt-get install python-simplejson - + Unzip and untar the source into a directory. Change to the directory -containing the program. Run: +containing the program. Run: + + $ make + +To start Plinth, run: + + $ ./start.sh - $ make - $ ./plinth.py - and point your web browser at `localhost:8000`. The default username is "admin" -and the default password is "secret". To actually edit the configuration of -your local/dev machine, also run: - - $ sudo ./exmachina/exmachina.py -v & +and the default password is "secret". ## Dependencies @@ -53,5 +53,3 @@ The documentation has some dependencies too. Documentation has been collected into a pdf that can be built using `make doc`. It also gets built into smaller files and other formats, including one suitable for install as a man page. - - diff --git a/start.sh b/start.sh new file mode 100755 index 000000000..01bc36e57 --- /dev/null +++ b/start.sh @@ -0,0 +1,8 @@ +#! /bin/sh + +PYTHONPATH=../exmachina:$PYTHONPATH + +sudo killall exmachina.py +sudo ../exmachina/exmachina.py -v & +python plinth.py +sudo killall exmachina.py From fed5bd13f1bdb5c1359c96512fd91e1dd445a9be Mon Sep 17 00:00:00 2001 From: Nick Daly Date: Sun, 11 Nov 2012 13:13:22 -0600 Subject: [PATCH 13/24] Externalized ExMachina again, partially reverting change: f1e764f2e5728113f191456236d02fdae6e7680a Partially revert the EM integration change, it's not solid yet. This allows EM to grow on its own. However, I'm not reverting the whole change because I want to make it easy to use EM from an external repository, and most of the Plinth-specific changes are good. To use EM in Plinth again, make sure EM and Plinth are in the same directory before running start.sh. The directory structure should look like: ./exmachina/ ./plinth/ start.sh updates Python's path correctly, so this change should be transparent and Plinth should still run the same. --- exmachina/HOWTO | 59 ----- exmachina/__init__.py | 0 exmachina/exmachina.py | 437 ------------------------------------ exmachina/init_test.sh | 13 -- exmachina/test_exmachina.py | 82 ------- 5 files changed, 591 deletions(-) delete mode 100644 exmachina/HOWTO delete mode 100644 exmachina/__init__.py delete mode 100755 exmachina/exmachina.py delete mode 100755 exmachina/init_test.sh delete mode 100755 exmachina/test_exmachina.py diff --git a/exmachina/HOWTO b/exmachina/HOWTO deleted file mode 100644 index f76b3448f..000000000 --- a/exmachina/HOWTO +++ /dev/null @@ -1,59 +0,0 @@ - -HOWTO: Use exmachina in Plinth to Change Configuration and Restart Services ------------------------------------------------------------------------------ - -exmachina is the name of a small and simple privilege separation layer that -lets Plinth (running as a web application with user/group www-data) edit -FreedomBox system configuration (which requires root privileges). The Augeas -library is used to provide a uniform interface to *most* configuration files in -/etc/, and a wrapper around init.d scripts allows services to be restarted -(which is often the way to have configuration changes go live). - -The exmachina daemon and client library is initialized when Plinth starts, and -is accessible in Plinth code through the "cfg" global variable: - - import cfg - cfg.exmachina.initd.restart("networking") - -Existing on-disk configuration settings can be read back through the "get" -action, and files/settings can be found with the "match" method. For example, -the following would print out a list of all the files in /etc/ with -augeas-editing support, and then print the system's hostname according to -/etc/hostname (a minimal one-line configuration file): - - for fname in c.call.augeas_match("/files/etc/*"): - print fname - cfg.exmachina.augeas.get("/files/etc/hostname/*") - -The mutating Augeas actions ("set", "insert", "remove", etc) operate on an -in-memory cache of the configuration files; to commit all changes (for all -modified files) to disk, use the "save" action. The following would set the -local hostname to fluffy.example.net, and then commit that change to disk. - - cfg.exmachina.augeas.set("/files/etc/hostname/*", "fluffy.example.net") - cfg.exmachina.augeas.save() - -After the above, the /etc/hostname file will be updated, but running network -services will not have picked up on the change. To have changes that have been -written to disk actually take effect, the "initd" module allows access to -restarting system services. In the case of hostnames, in debian there exists a -helper script called "/etc/init.d/hostname.sh", so the following would be used -to update all network daemons to a new hostname: - - cfg.exmachina.initd.restart("hostname.sh") - -Augeas makes some effort to prevent partial writes or configuration file -corruption, and in some cases provides basic type checking. However, it does -not do any logical error detection or correction. For example, two separate -services could be configured to try and listen on the same port, or an invalid -hostname could be specified (eg, "&.^$#"). Application code in Plinth is -responsible for ensuring the logical correctness of configuration changes. - -If there is a serious problem executing an exmachina method call (eg, tried to -restart a non-existent service), a bjsonrpc.exceptions.ServerError exception -will be raised, with the actual exception message (but not the traceback) -passed through. - -Reading: -- Augeas API notes: http://augeas.net/docs/api.html -- exmachina repository: https://github.com/bnewbold/exmachina diff --git a/exmachina/__init__.py b/exmachina/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/exmachina/exmachina.py b/exmachina/exmachina.py deleted file mode 100755 index 21de49322..000000000 --- a/exmachina/exmachina.py +++ /dev/null @@ -1,437 +0,0 @@ -#!/usr/bin/env python - -""" -Author: bnewbold -Date: July 2012 -License: GPLv3 (see http://www.gnu.org/licenses/gpl-3.0.html) - (two helper functions copied from web, as cited below) -Package Requirements: python-augeas, bjsonrpc - -This file implements both ends (privilaged daemon and unprivilaged python -client library) of a crude system configuration message bus, intended for use -(initially) with the Plinth web interface to the FreedomBox operating system. - -The goal is to provide partially-untrusted processes (such as the web interface -running as the www-data user) access to core system configuration files -(through the Augeas library) and daemon control (through the init.d scripts). - -The daemon process (started in the same startup script as Plinth) runs as root -and accepts JSON-RPC method calls through a unix domain socket -(/tmp/exmachina.sock by default). Because file access control may not be -sufficiently flexible for access control, a somewhat-elaborate secret key -mechanism can be used to control access to the RPC mechanism. - -The (optional) shared secret-key mechanism requires clients to first call the -"authenticate" RPC method before any other methods. The secret key is passed to -the server process through stdin at startup (command line arguments could be -snooped by unprivilaged processes), and would presumably be passed on to the -client in the same way. The init_test.sh script demonstrates this mechanism. -""" - -import os -import sys -import grp -import shutil -import argparse -import logging -import socket -import subprocess -import time -import base64 - -import bjsonrpc -import bjsonrpc.handlers -import bjsonrpc.server -import augeas - -log = logging.getLogger(__name__) - - -def execute_service(servicename, action, timeout=10): - """This function mostly ripped from StackOverflow: - http://stackoverflow.com/questions/1556348/python-run-a-process-with-timeout-and-capture-stdout-stderr-and-exit-status - """ - # ensure service name isn't tricky trick - script = "/etc/init.d/" + os.path.split(servicename)[1] - - if not os.path.exists(script): - raise ValueError("so such service: %s" % servicename) - - command_list = [script, action] - log.info("executing: %s" % command_list) - proc = subprocess.Popen(command_list, - bufsize=0, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - poll_seconds = .250 - deadline = time.time() + timeout - while time.time() < deadline and proc.poll() is None: - time.sleep(poll_seconds) - - if proc.poll() is None: - if float(sys.version[:3]) >= 2.6: - proc.terminate() - raise Exception("execution timed out (>%d seconds): %s" % - (timeout, command_list)) - - stdout, stderr = proc.communicate() - # TODO: should raise exception here if proc.returncode != 0? - return stdout, stderr, proc.returncode - -def execute_apt(packagename, action, timeout=120, aptargs=['-q', '-y']): - # ensure package name isn't tricky trick - if action != "update" \ - and (packagename != packagename.strip().split()[0] \ - or packagename.startswith('-')): - raise ValueError("Not a good apt package name: %s" % packagename) - - if action == "update": - command_list = ['apt-get', action] - else: - command_list = ['apt-get', action, packagename] - command_list.extend(aptargs) - log.info("executing: %s" % command_list) - proc = subprocess.Popen(command_list, - bufsize=0, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - poll_seconds = .250 - deadline = time.time() + timeout - while time.time() < deadline and proc.poll() is None: - time.sleep(poll_seconds) - - if proc.poll() is None: - if float(sys.version[:3]) >= 2.6: - proc.terminate() - raise Exception("execution timed out (>%d seconds): %s" % - (timeout, command_list)) - - stdout, stderr = proc.communicate() - return stdout, stderr, proc.returncode - - -class ExMachinaHandler(bjsonrpc.handlers.BaseHandler): - - # authentication state variable. If not None, still need to authenticate; - # if None then authentication not require or was already successful for - # this instantiation of the Handler. This class variable gets optionally - # overridden on a per-process basis - secret_key = None - - def _setup(self): - self.augeas = augeas.Augeas() - - def authenticate(self, secret_key): - if not self.secret_key: - log.warn("Unecessary authentication attempt") - return - if not secret_key.strip() == self.secret_key.strip(): - # fail hard - log.error("Authentication failed!") - sys.exit() - self.secret_key = None - - # ------------- Augeas API Passthrough ----------------- - def augeas_save(self): - if not self.secret_key: - log.info("augeas: saving config") - return self.augeas.save() - - def augeas_set(self, path, value): - if not self.secret_key: - log.info("augeas: set %s=%s" % (path, value)) - return self.augeas.set(path.encode('utf-8'), - value.encode('utf-8')) - - def augeas_setm(self, base, sub, value): - if not self.secret_key: - log.info("augeas: setm %s %s = %s" % (base, sub, value)) - return self.augeas.setm(base.encode('utf-8'), - sub.encode('utf-8'), - value.encode('utf-8')) - - def augeas_get(self, path): - if not self.secret_key: - # reduce verbosity - log.debug("augeas: get %s" % path) - return self.augeas.get(path.encode('utf-8')) - - def augeas_match(self, path): - if not self.secret_key: - # reduce verbosity - log.debug("augeas: match %s" % path) - return self.augeas.match("%s" % path.encode('utf-8')) - - def augeas_insert(self, path, label, before=True): - if not self.secret_key: - log.info("augeas: insert %s=%s" % (path, value)) - return self.augeas.insert(path.encode('utf-8'), - label.encode('utf-8'), - before=before) - - def augeas_move(self, src, dst): - if not self.secret_key: - log.info("augeas: move %s -> %s" % (src, dst)) - return self.augeas.move(src.encode('utf-8'), dst.encode('utf-8')) - - def augeas_remove(self, path): - if not self.secret_key: - log.info("augeas: remove %s" % path) - return self.augeas.remove(path.encode('utf-8')) - - # ------------- Misc. non-Augeas Helpers ----------------- - def set_timezone(self, tzname): - if not self.secret_key: - log.info("reset timezone to %s" % tzname) - tzname = tzname.strip() - tzpath = os.path.join("/usr/share/zoneinfo", tzname) - try: - os.stat(tzpath) - except OSError: - # file not found - raise ValueError("timezone not valid: %s" % tzname) - shutil.copy( - os.path.join("/usr/share/zoneinfo", tzname), - "/etc/localtime") - with open("/etc/timezone", "w") as tzfile: - tzfile.write(tzname + "\n") - return "timezone changed to %s" % tzname - - # ------------- init.d Service Control ----------------- - def initd_status(self, servicename): - if not self.secret_key: - return execute_service(servicename, "status") - - def initd_start(self, servicename): - if not self.secret_key: - return execute_service(servicename, "start") - - def initd_stop(self, servicename): - if not self.secret_key: - return execute_service(servicename, "stop") - - def initd_restart(self, servicename): - if not self.secret_key: - return execute_service(servicename, "restart") - - # ------------- apt-get Package Control ----------------- - def apt_install(self, packagename): - if not self.secret_key: - return execute_apt(packagename, "install") - - def apt_update(self): - if not self.secret_key: - return execute_apt("", "update") - - def apt_remove(self, packagename): - if not self.secret_key: - return execute_apt(packagename, "remove") - - -class EmptyClass(): - # Used by ExMachinaClient below - pass - - -class ExMachinaClient(): - """Simple client wrapper library to expose augeas and init.d methods. - - In brief, use augeas.get/set/insert to modify system configuration files - under the /files/etc/* namespace. augeas.match with a wildcard can be used - to find variables to edit. - - After making any changes, use augeas.save to commit to disk, then - initd.restart to restart the appropriate system daemons. In many cases, - this would be the 'networking' meta-daemon. - - See test_exmachina.py for some simple examples; see the augeas docs for - more in depth guidance. - """ - - def __init__(self, - socket_path="/tmp/exmachina.sock", - secret_key=None): - self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self.sock.connect(socket_path) - self.conn = bjsonrpc.connection.Connection(self.sock) - - if secret_key: - self.conn.call.authenticate(secret_key) - - self.augeas = EmptyClass() - self.initd = EmptyClass() - self.apt = EmptyClass() - self.misc = EmptyClass() - - self.augeas.save = self.conn.call.augeas_save - self.augeas.set = self.conn.call.augeas_set - self.augeas.setm = self.conn.call.augeas_setm - self.augeas.get = self.conn.call.augeas_get - self.augeas.match = self.conn.call.augeas_match - self.augeas.insert = self.conn.call.augeas_insert - self.augeas.move = self.conn.call.augeas_move - self.augeas.remove = self.conn.call.augeas_remove - self.initd.status = self.conn.call.initd_status - self.initd.start = self.conn.call.initd_start - self.initd.stop = self.conn.call.initd_stop - self.initd.restart = self.conn.call.initd_restart - self.apt.install = self.conn.call.apt_install - self.apt.update = self.conn.call.apt_update - self.apt.remove = self.conn.call.apt_remove - self.misc.set_timezone = self.conn.call.set_timezone - - def close(self): - self.sock.close() - - -def run_server(socket_path, secret_key=None, socket_group=None): - - if not 0 == os.geteuid(): - log.warn("Expected to be running as root!") - - if os.path.exists(socket_path): - os.unlink(socket_path) - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.bind(socket_path) - sock.listen(1) - - if socket_group is not None: - socket_uid = os.stat(socket_path).st_uid - socket_gid = grp.getgrnam(socket_group).gr_gid - os.chmod(socket_path, 0660) - os.chown(socket_path, socket_uid, socket_gid) - else: - os.chmod(socket_path, 0666) - if secret_key: - ExMachinaHandler.secret_key = secret_key - - serv = bjsonrpc.server.Server(sock, handler_factory=ExMachinaHandler) - serv.serve() - -def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): - """ - From: http://www.noah.org/wiki/Daemonize_Python - - This forks the current process into a daemon. The stdin, stdout, and - stderr arguments are file names that will be opened and be used to replace - the standard file descriptors in sys.stdin, sys.stdout, and sys.stderr. - These arguments are optional and default to /dev/null. Note that stderr is - opened unbuffered, so if it shares a file with stdout then interleaved - output may not appear in the order that you expect. """ - - # Do first fork. - try: - pid = os.fork() - if pid > 0: - sys.exit(0) # Exit first parent. - except OSError, e: - sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror)) - sys.exit(1) - - # Decouple from parent environment. - os.chdir("/") - os.umask(0) - os.setsid() - - # Do second fork. - try: - pid = os.fork() - if pid > 0: - sys.exit(0) # Exit second parent. - except OSError, e: - sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror)) - sys.exit(1) - - # Now I am a daemon! - - # Redirect standard file descriptors. - si = open(stdin, 'r') - so = open(stdout, 'a+') - se = open(stderr, 'a+', 0) - os.dup2(si.fileno(), sys.stdin.fileno()) - os.dup2(so.fileno(), sys.stdout.fileno()) - os.dup2(se.fileno(), sys.stderr.fileno()) - return pid - -# ============================================================================= -# Command line handling -def main(): - - global log - parser = argparse.ArgumentParser(usage= - "usage: %prog [options]\n" - "%prog --help for more info." - ) - parser.add_argument("-v", "--verbose", - default=False, - help="Show more debugging statements", - action="store_true") - parser.add_argument("-q", "--quiet", - default=False, - help="Show fewer informational statements", - action="store_true") - parser.add_argument("-k", "--key", - default=False, - help="Wait for Secret Access Key on stdin before starting", - action="store_true") - parser.add_argument("--random-key", - default=False, - help="Just dump a random base64 key and exit", - action="store_true") - parser.add_argument("-s", "--socket-path", - default="/tmp/exmachina.sock", - help="UNIX Domain socket file path to listen on", - metavar="FILE") - parser.add_argument("--pidfile", - default=None, - help="Daemonize and write pid to this file", - metavar="FILE") - parser.add_argument("-g", "--group", - default=None, - help="chgrp socket file to this group and set 0660 permissions") - - args = parser.parse_args() - - #if len(args) != 0: - #parser.error("Incorrect number of arguments") - - if args.random_key: - sys.stdout.write(base64.urlsafe_b64encode(os.urandom(128))) - sys.exit(0) - - log = logging.getLogger() - hdlr = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') - hdlr.setFormatter(formatter) - log.addHandler(hdlr) - - if args.verbose: - log.setLevel(logging.DEBUG) - elif args.quiet: - log.setLevel(logging.ERROR) - else: - log.setLevel(logging.INFO) - - secret_key = None - if args.key: - log.debug("Waiting for secret key on stdin...") - secret_key = sys.stdin.readline().strip() - log.debug("Got it!") - - if args.pidfile: - with open(args.pidfile, 'w') as pfile: - # ensure file is available/writable - pass - os.unlink(args.pidfile) - daemonize() - pid = os.getpid() - with open(args.pidfile, 'w') as pfile: - pfile.write("%s" % pid) - log.info("Daemonized, pid is %s" % pid) - - run_server(secret_key=secret_key, - socket_path=args.socket_path, - socket_group=args.group) - -if __name__ == '__main__': - main() diff --git a/exmachina/init_test.sh b/exmachina/init_test.sh deleted file mode 100755 index 941285d36..000000000 --- a/exmachina/init_test.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -# Test init.d-style initialization; run this script as root (or sudo it) - -export key=`./exmachina.py --random-key` - -echo $key | ./exmachina.py -vk --pidfile /tmp/exmachina_test.pid -g www-data -sleep 1 -echo $key | sudo -u www-data -g www-data ./test_exmachina.py -k - -kill `cat /tmp/exmachina_test.pid` && rm /tmp/exmachina_test.pid -sleep 1 -jobs diff --git a/exmachina/test_exmachina.py b/exmachina/test_exmachina.py deleted file mode 100755 index 6c1db9418..000000000 --- a/exmachina/test_exmachina.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python - -""" -This file tests the "client side" of the exmachina layer. - -To use with secret keys, do the following in seperate terminals: - - $ echo "" | sudo ./exmachina.py -vk - $ echo "" | ./test_exmachina.py -k - -To use without, do the following in seperate terminals: - - $ sudo ./exmachina.py -v - $ ./test_exmachina.py - -Use the init_test.sh script to test shared key passing and privilage seperation -at the same time: - - $ sudo ./init_test.sh -""" - -import sys -import socket - -import bjsonrpc -import bjsonrpc.connection -from bjsonrpc.exceptions import ServerError - -from exmachina import ExMachinaClient - -# ============================================================================= -# Command line handling -def main(): - - socket_path = "/tmp/exmachina.sock" - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(socket_path) - - secret_key = None - if sys.argv[-1] == "-k": - print "waiting for key on stdin..." - secret_key = sys.stdin.readline() - print "sent!" - - print "========= Testing JSON-RPC connection" - c = bjsonrpc.connection.Connection(sock) - if secret_key: - c.call.authenticate(secret_key) - print "/*: %s" % c.call.augeas_match("/*") - print "/augeas/*: %s" % c.call.augeas_match("/augeas/*") - print "/etc/* files:" - for name in c.call.augeas_match("/files/etc/*"): - print "\t%s" % name - print c.call.initd_status("bluetooth") - print "hostname: %s" % c.call.augeas_get("/files/etc/hostname/*") - print "localhost: %s" % c.call.augeas_get("/files/etc/hosts/1/canonical") - sock.close() - - print "========= Testing user client library" - client = ExMachinaClient(secret_key=secret_key) - print client.augeas.match("/files/etc/*") - #print client.initd.restart("bluetooth") - try: - print client.initd.status("greentooth") - print "ERROR: should have failed above!" - except ServerError: - print "(got expected error, good!)" - print "(expect Error on the above line)" - print client.initd.status("bluetooth") - print client.apt.install("pkg_which_does_not_exist") - print client.apt.remove("pkg_which_does_not_exist") - #print client.apt.update() # can be slow... - #print client.misc.set_timezone("UTC") # don't clobber system... - try: - print client.misc.set_timezone("whoopie") # should be an error - print "ERROR: should have failed above!" - except ServerError: - print "(got expected error, good!)" - client.close() - -if __name__ == '__main__': - main() From b2c157ec1dafe4d5d5f35545b18161ee707d3767 Mon Sep 17 00:00:00 2001 From: Tom Galloway Date: Thu, 3 Jan 2013 15:08:06 +0000 Subject: [PATCH 14/24] Added predepend packages and updated python path for build folders. --- .gitignore | 3 ++- Makefile | 18 +++++++++++++++++- plinth.py | 2 +- start.sh | 7 +++++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 570bf0b09..3b943e1bb 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ TODO cfg.py cherrypy.config data/users.sqlite3 - +predepend +build/ diff --git a/Makefile b/Makefile index 3afdbcfb0..cf10e973c 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ #SHELL := /bin/bash MAKE=make +BUILD_DIR = build #TODO: add install target @@ -9,9 +10,22 @@ COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS)) PWD=`pwd` ## Catch-all tagets -default: cfg cherrypy.config dirs template css docs dbs +default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina $(BUILD_DIR)/bjsonrpc all: default +build: + mkdir -p $(BUILD_DIR) + +predepend: + sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson" + touch predepend + +$(BUILD_DIR)/exmachina: build + git clone git://github.com/bnewbold/exmachina $(BUILD_DIR)/exmachina + +$(BUILD_DIR)/bjsonrpc: build + git clone git://github.com/deavid/bjsonrpc.git $(BUILD_DIR)/bjsonrpc + dbs: data/users.sqlite3 data/users.sqlite3: data/users.sqlite3.distrib @@ -69,3 +83,5 @@ clean: @find . -name "*.bak" -exec rm {} \; @$(MAKE) -s -C doc clean @$(MAKE) -s -C templates clean + rm -rf build + rm -f predepend diff --git a/plinth.py b/plinth.py index 6c197463c..eb39a4251 100755 --- a/plinth.py +++ b/plinth.py @@ -17,7 +17,7 @@ from util import * from logger import Logger #from modules.auth import AuthController, require, member_of, name_is -from exmachina.exmachina import ExMachinaClient +from exmachina import ExMachinaClient import socket __version__ = "0.2.14" diff --git a/start.sh b/start.sh index 01bc36e57..1a16d1a5f 100755 --- a/start.sh +++ b/start.sh @@ -1,8 +1,11 @@ #! /bin/sh -PYTHONPATH=../exmachina:$PYTHONPATH +PYTHONPATH=build/exmachina:$PYTHONPATH +PYTHONPATH=build/bjsonrpc:$PYTHONPATH + +export PYTHONPATH sudo killall exmachina.py -sudo ../exmachina/exmachina.py -v & +sudo build/exmachina/exmachina.py -v & python plinth.py sudo killall exmachina.py From 076bf829e72676575a3d44f53722cbfcb6e03fde Mon Sep 17 00:00:00 2001 From: Tom Galloway Date: Thu, 3 Jan 2013 15:12:15 +0000 Subject: [PATCH 15/24] Extra predepend --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cf10e973c..b28db0a17 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ build: mkdir -p $(BUILD_DIR) predepend: - sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson" + sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc" touch predepend $(BUILD_DIR)/exmachina: build From 847d000f998d05afc7eef589319159778ff3d5de Mon Sep 17 00:00:00 2001 From: Tom Galloway Date: Thu, 3 Jan 2013 15:20:37 +0000 Subject: [PATCH 16/24] Plinth needs to be run as root currently. --- start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.sh b/start.sh index 1a16d1a5f..da7a4d34e 100755 --- a/start.sh +++ b/start.sh @@ -7,5 +7,5 @@ export PYTHONPATH sudo killall exmachina.py sudo build/exmachina/exmachina.py -v & -python plinth.py +sudo python plinth.py sudo killall exmachina.py From 95118b3b96d18c5255f1d273b07c387058b32e09 Mon Sep 17 00:00:00 2001 From: Tom Galloway Date: Thu, 3 Jan 2013 15:22:16 +0000 Subject: [PATCH 17/24] Clean now uses BUILD_DIR variable. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b28db0a17..eb1fad1ce 100644 --- a/Makefile +++ b/Makefile @@ -83,5 +83,5 @@ clean: @find . -name "*.bak" -exec rm {} \; @$(MAKE) -s -C doc clean @$(MAKE) -s -C templates clean - rm -rf build + rm -rf $(BUILD_DIR) rm -f predepend From b45d5cfe85bc7686a6296aa0af4735710a6f7122 Mon Sep 17 00:00:00 2001 From: Tom Galloway Date: Fri, 4 Jan 2013 12:50:51 +0000 Subject: [PATCH 18/24] Extra predepend's, plinth doesn't need to run as root, pidfile location set in cfg file. --- Makefile | 2 +- cfg.sample.py | 1 + plinth.py | 3 ++- start.sh | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index eb1fad1ce..6c2f8ac8c 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ build: mkdir -p $(BUILD_DIR) predepend: - sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc" + sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc python-cheetah" touch predepend $(BUILD_DIR)/exmachina: build diff --git a/cfg.sample.py b/cfg.sample.py index 160272446..8521ca653 100644 --- a/cfg.sample.py +++ b/cfg.sample.py @@ -8,6 +8,7 @@ user_db = os.path.join(data_dir, "users") status_log_file = os.path.join(data_dir, "status.log") access_log_file = os.path.join(data_dir, "access.log") users_dir = os.path.join(data_dir, "users") +pidfile = os.path.join(data_dir, "pidfile.pid") product_name = "Plinth" box_name = "FreedomBox" diff --git a/plinth.py b/plinth.py index eb39a4251..3b8a1001a 100755 --- a/plinth.py +++ b/plinth.py @@ -80,7 +80,8 @@ def parse_arguments(): if args.pidfile: cfg.pidfile = args.pidfile else: - cfg.pidfile = "plinth.pid" + if not cfg.pidfile: + cfg.pidfile = "plinth.pid" if args.listen_exmachina_key: # this is where we optionally try to read in a shared secret key to # authenticate connections to exmachina diff --git a/start.sh b/start.sh index da7a4d34e..1a16d1a5f 100755 --- a/start.sh +++ b/start.sh @@ -7,5 +7,5 @@ export PYTHONPATH sudo killall exmachina.py sudo build/exmachina/exmachina.py -v & -sudo python plinth.py +python plinth.py sudo killall exmachina.py From 8e16374136b5d230edc1cb049f30dc4ca6e88f78 Mon Sep 17 00:00:00 2001 From: Nick Daly Date: Sun, 6 Jan 2013 16:05:58 -0600 Subject: [PATCH 19/24] Commented out the extra bjsonrpc call. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6c2f8ac8c..40460de7a 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS)) PWD=`pwd` ## Catch-all tagets -default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina $(BUILD_DIR)/bjsonrpc +default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina #$(BUILD_DIR)/bjsonrpc all: default build: From b54a4906e67e6e35a571c754865c1a517722674f Mon Sep 17 00:00:00 2001 From: Tom Galloway Date: Thu, 10 Jan 2013 09:15:31 +0000 Subject: [PATCH 20/24] Fix to allow hostname to be updated on Ubuntu. This needed a change to exmachina which I've created a pull request for. I've updated Make to point to my fork of exmachina until this has been merged, when it can be reverted. --- Makefile | 2 +- modules/installed/system/config.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 40460de7a..72d916965 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ predepend: touch predepend $(BUILD_DIR)/exmachina: build - git clone git://github.com/bnewbold/exmachina $(BUILD_DIR)/exmachina + git clone git://github.com/tomgalloway/exmachina $(BUILD_DIR)/exmachina $(BUILD_DIR)/bjsonrpc: build git clone git://github.com/deavid/bjsonrpc.git $(BUILD_DIR)/bjsonrpc diff --git a/modules/installed/system/config.py b/modules/installed/system/config.py index 01c4ec693..b671fcc12 100644 --- a/modules/installed/system/config.py +++ b/modules/installed/system/config.py @@ -10,6 +10,7 @@ import cfg from forms import Form from model import User from util import * +import platform class Config(PagePlugin): def __init__(self, *args, **kwargs): @@ -49,7 +50,10 @@ def set_hostname(hostname): # don't persist/cache change unless it was saved successfuly sys_store = filedict_con(cfg.store_file, 'sys') sys_store['hostname'] = hostname - cfg.exmachina.initd.restart("hostname.sh") # is hostname.sh debian-only? + if platform.linux_distribution()[0]=="Ubuntu" : + cfg.exmachina.service.start("hostname") + else: + cfg.exmachina.initd.restart("hostname.sh") # is hostname.sh debian-only? except OSError, e: raise cherrypy.HTTPError(500, "Hostname restart failed: %s" % e) From 646b5518bbd33c6979cac548adca2416d401833a Mon Sep 17 00:00:00 2001 From: Tom Galloway Date: Wed, 16 Jan 2013 13:08:48 +0000 Subject: [PATCH 21/24] withsqlite is now retrieved from github. Manage User & Groups pages now display correctly but don't do anything yet. --- .gitignore | 1 + Makefile | 9 ++++++++- modules/installed/system/users.py | 19 ++++++++++--------- start.sh | 1 - 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 3b943e1bb..d5be6856d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ cherrypy.config data/users.sqlite3 predepend build/ +*.pid \ No newline at end of file diff --git a/Makefile b/Makefile index 72d916965..f5bf865b9 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ #SHELL := /bin/bash MAKE=make BUILD_DIR = build +VENDOR_DIR = vendor #TODO: add install target @@ -10,12 +11,15 @@ COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS)) PWD=`pwd` ## Catch-all tagets -default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina #$(BUILD_DIR)/bjsonrpc +default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina $(VENDOR_DIR)/withsqlite #$(BUILD_DIR)/bjsonrpc all: default build: mkdir -p $(BUILD_DIR) +vendor: + mkdir -p $(VENDOR_DIR) + predepend: sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc python-cheetah" touch predepend @@ -23,6 +27,9 @@ predepend: $(BUILD_DIR)/exmachina: build git clone git://github.com/tomgalloway/exmachina $(BUILD_DIR)/exmachina +$(VENDOR_DIR)/withsqlite: vendor + git clone git://github.com/jvasile/withsqlite.git $(VENDOR_DIR)/withsqlite + $(BUILD_DIR)/bjsonrpc: build git clone git://github.com/deavid/bjsonrpc.git $(BUILD_DIR)/bjsonrpc diff --git a/modules/installed/system/users.py b/modules/installed/system/users.py index c4ac97771..81e747b2f 100644 --- a/modules/installed/system/users.py +++ b/modules/installed/system/users.py @@ -5,22 +5,23 @@ from plugin_mount import PagePlugin, FormPlugin import cfg from forms import Form from util import * +from pprint import pprint class users(PagePlugin): order = 20 # order of running init in PagePlugins def __init__(self, *args, **kwargs): PagePlugin.__init__(self, *args, **kwargs) self.register_page("sys.users") + self.register_page("sys.users.add") + self.register_page("sys.users.edit") @cherrypy.expose @require() def index(self): - parts = self.forms('/sys/config') - parts['title']=_("Manage Users and Groups") - return self.fill_template(**parts) + return self.fill_template(title="Manage Users and Groups", sidebar_right="""Add User
Edit Users""") class add(FormPlugin, PagePlugin): - url = ["/sys/users"] + url = ["/sys/users/add"] order = 30 sidebar_left = '' @@ -63,10 +64,10 @@ class add(FormPlugin, PagePlugin): msg = add_message(msg, "%s saved." % username) main = self.make_form(username, name, email, message=msg) - return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) + return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) class edit(FormPlugin, PagePlugin): - url = ["/sys/users"] + url = ["/sys/users/edit"] order = 35 sidebar_left = '' @@ -77,7 +78,7 @@ class edit(FormPlugin, PagePlugin): system.

Deleting users is permanent!

""" % (cfg.product_name, cfg.box_name)) def main(self, msg=''): - users = cfg.users.keys() + users = cfg.users add_form = Form(title=_("Edit or Delete User"), action="/sys/users/edit", message=msg) add_form.html('Delete
') for uname in sorted(users.keys()): @@ -114,7 +115,7 @@ class edit(FormPlugin, PagePlugin): else: msg.add = _("Must specify at least one valid, existing user.") main = self.make_form(msg=msg.text) - return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) + return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) sidebar_right = '' u = cfg.users[kwargs['username']] @@ -125,4 +126,4 @@ class edit(FormPlugin, PagePlugin): main = _("""Edit User '%s'""" % u['username']) sidebar_right = '' - return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=sidebar_right) + return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=sidebar_right) diff --git a/start.sh b/start.sh index 1a16d1a5f..4be20c1b6 100755 --- a/start.sh +++ b/start.sh @@ -1,7 +1,6 @@ #! /bin/sh PYTHONPATH=build/exmachina:$PYTHONPATH -PYTHONPATH=build/bjsonrpc:$PYTHONPATH export PYTHONPATH From a312b6d288dc2f978237c1d5e309972d3c873327 Mon Sep 17 00:00:00 2001 From: Tom Galloway Date: Wed, 16 Jan 2013 15:05:04 +0000 Subject: [PATCH 22/24] Removed changes to get withsqlite from github as it's a different version? --- Makefile | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Makefile b/Makefile index f5bf865b9..72d916965 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ #SHELL := /bin/bash MAKE=make BUILD_DIR = build -VENDOR_DIR = vendor #TODO: add install target @@ -11,15 +10,12 @@ COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS)) PWD=`pwd` ## Catch-all tagets -default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina $(VENDOR_DIR)/withsqlite #$(BUILD_DIR)/bjsonrpc +default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina #$(BUILD_DIR)/bjsonrpc all: default build: mkdir -p $(BUILD_DIR) -vendor: - mkdir -p $(VENDOR_DIR) - predepend: sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc python-cheetah" touch predepend @@ -27,9 +23,6 @@ predepend: $(BUILD_DIR)/exmachina: build git clone git://github.com/tomgalloway/exmachina $(BUILD_DIR)/exmachina -$(VENDOR_DIR)/withsqlite: vendor - git clone git://github.com/jvasile/withsqlite.git $(VENDOR_DIR)/withsqlite - $(BUILD_DIR)/bjsonrpc: build git clone git://github.com/deavid/bjsonrpc.git $(BUILD_DIR)/bjsonrpc From c4cddbfc0e42afb8e69dce08b561ac00b0f07b35 Mon Sep 17 00:00:00 2001 From: Tom Galloway Date: Mon, 21 Jan 2013 10:30:52 +0000 Subject: [PATCH 23/24] Changes to get user management screens started. Updated UserStore to add all expected functions. Added tests for these functions. --- model.py | 9 +-- modules/installed/lib/user_store.py | 38 +++++++++++- modules/installed/system/users.py | 24 ++++---- test.sh | 9 +++ tests/test_user_store.py | 86 ++++++++++++++++++++++++++++ tests/testdata/users.sqlite3 | Bin 0 -> 3072 bytes 6 files changed, 148 insertions(+), 18 deletions(-) create mode 100755 test.sh create mode 100644 tests/test_user_store.py create mode 100644 tests/testdata/users.sqlite3 diff --git a/model.py b/model.py index d3807d0b1..e0b3cd20d 100644 --- a/model.py +++ b/model.py @@ -1,14 +1,15 @@ class User(dict): - """ Every user must have keys for a username, name, password (this + """ Every user must have keys for a username, name, passphrase (this is a md5 hash of the password), groups, and an email address. They can be blank or None, but the keys must exist. """ def __init__(self, dict=None): - for key in ['username', 'name', 'password', 'email']: + for key in ['username', 'name', 'passphrase', 'email']: self[key] = '' for key in ['groups']: self[key] = [] - for key in dict: - self[key] = dict[key] + if dict: + for key in dict: + self[key] = dict[key] def __getattr__(self, attr): return None diff --git a/modules/installed/lib/user_store.py b/modules/installed/lib/user_store.py index a4042c102..236b73a8f 100644 --- a/modules/installed/lib/user_store.py +++ b/modules/installed/lib/user_store.py @@ -12,10 +12,42 @@ class UserStore(UserStoreModule, sqlite_db): self.db_file = cfg.user_db sqlite_db.__init__(self, self.db_file, autocommit=True) self.__enter__() + def close(self): - self.__exit__() - def expert(self): - return False + self.__exit__(None,None,None) + + def expert(self, username=None): + groups = self.attr(username,"groups") + if not groups: + return False + return 'expert' in groups + + def attr(self, username=None, field=None): + return self.get(username)[field] + + def get(self,username=None): + return User(sqlite_db.get(self,username)) + + def exists(self, username=None): + try: + user = self.get(username) + if not user: + return False + elif user["username"]=='': + return False + return True + except TypeError: + return False + + def remove(self,username=None): + self.__delitem__(username) + self.commit() + + def get_all(self): + return self.items() + + def set(self,username=None,user=None): + sqlite_db.__setitem__(self,username, user) class UserStoreOld(): #class UserStore(UserStoreModule): diff --git a/modules/installed/system/users.py b/modules/installed/system/users.py index 81e747b2f..63d9c7631 100644 --- a/modules/installed/system/users.py +++ b/modules/installed/system/users.py @@ -47,23 +47,25 @@ class add(FormPlugin, PagePlugin): return form.render() def process_form(self, username=None, name=None, email=None, md5_password=None, **kwargs): - msg = '' + msg = Message() - if not username: msg = add_message(msg, _("Must specify a username!")) - if not md5_password: msg = add_message(msg, _("Must specify a password!")) + if not username: msg.add = _("Must specify a username!") + if not md5_password: msg.add = _("Must specify a password!") - if username in cfg.users: - msg = add_message(msg, _("User already exists!")) + if username in cfg.users.keys(): + msg.add = _("User already exists!") else: try: - cfg.users[username]= User(dict={'username':username, 'name':name, 'email':email, 'password':md5_password}) + di = {'username':username, 'name':name, 'email':email, 'passphrase':md5_password} + new_user = User(dict=di) + cfg.users.set(username,new_user) except: - msg = add_message(msg, _("Error storing user!")) + msg.add = _("Error storing user!") if not msg: - msg = add_message(msg, "%s saved." % username) - - main = self.make_form(username, name, email, message=msg) + msg.add = _("%s saved." % username) + cfg.log(msg.text) + #main = self.make_form(username, name, email, msg=msg.text) return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) class edit(FormPlugin, PagePlugin): @@ -114,7 +116,7 @@ class edit(FormPlugin, PagePlugin): msg.add(_("User %s does not exist." % username)) else: msg.add = _("Must specify at least one valid, existing user.") - main = self.make_form(msg=msg.text) + #main = self.make_form(msg=msg.text) return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) sidebar_right = '' diff --git a/test.sh b/test.sh new file mode 100755 index 000000000..4b6f701d4 --- /dev/null +++ b/test.sh @@ -0,0 +1,9 @@ +#! /bin/sh + +PYTHONPATH=build/exmachina:$PYTHONPATH +PYTHONPATH=modules/installed/lib:$PYTHONPATH +PYTHONPATH=vendor:$PYTHONPATH + +export PYTHONPATH + +python tests/test_user_store.py diff --git a/tests/test_user_store.py b/tests/test_user_store.py new file mode 100644 index 000000000..e6a3e8433 --- /dev/null +++ b/tests/test_user_store.py @@ -0,0 +1,86 @@ +#! /usr/bin/env python +# -*- mode: python; mode: auto-fill; fill-column: 80 -*- + +import user_store +from logger import Logger +import cfg +import unittest +import cherrypy +import plugin_mount +import os +from model import User +cfg.log = Logger() + +cherrypy.log.access_file = None + +class UserStore(unittest.TestCase): + """Test each function of user_store to confirm they work as expected""" + + def setUp(self): + cfg.user_db = os.path.join(cfg.file_root, "tests/testdata/users"); + self.userstore = plugin_mount.UserStoreModule.get_plugins()[0] + + def tearDown(self): + for user in self.userstore.get_all(): + self.userstore.remove(user[0]) + self.userstore.close() + + def test_user_does_not_exist(self): + self.assertEqual(self.userstore.exists("notausername"),False) + + def test_user_does_exist(self): + self.add_user("isausername", False) + self.assertEqual(self.userstore.exists("isausername"),True) + + def test_add_user(self): + self.assertEqual(len(self.userstore.items()),0) + self.add_user("test_user", False) + self.assertEqual(len(self.userstore.items()),1) + + def test_user_is_in_expert_group(self): + self.add_user("test_user", True) + self.assertEqual(self.userstore.expert("test_user"),True) + + def test_user_is_not_in_expert_group(self): + self.add_user("test_user", False) + self.assertEqual(self.userstore.expert("test_user"),False) + + def test_user_removal(self): + self.assertEqual(len(self.userstore.items()),0) + self.add_user("test_user", False) + self.assertEqual(len(self.userstore.items()),1) + self.userstore.remove("test_user") + self.assertEqual(len(self.userstore.items()),0) + + def test_get_user_email_attribute(self): + self.add_user("test_user", False,"test@home") + self.assertEqual(self.userstore.attr("test_user","email"),"test@home") + + def test_get_user(self): + test_user = self.add_user("test_user", False) + self.assertEqual(self.userstore.get("test_user"),test_user) + + def test_get_all_users(self): + self.add_user("test_user1", False) + self.add_user("test_user2", False) + self.assertEqual(len(self.userstore.get_all()),2) + + def add_user(self, test_username, add_to_expert_group, email=''): + test_user = self.create_user(test_username, email) + if add_to_expert_group: + test_user = self.add_user_to_expert_group(test_user) + self.userstore.set(test_username,test_user) + return test_user + + def create_user(self, username, email=''): + test_user = User() + test_user["username"] = username + test_user["email"] = email + return test_user + + def add_user_to_expert_group(self, user): + user["groups"] = ["expert"] + return user + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/testdata/users.sqlite3 b/tests/testdata/users.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..03782a7d7f4282049ab931c01f0df26b5534545f GIT binary patch literal 3072 zcmWFz^vNtqRY=P(%1ta$FlJz3U}R))P*7lCU`%FUU|@ zBSl{3KMYLF-3-j#%oouVjB-aqU^E0EA>hKuCN3_{m|2pTl#^Oql3xTNn4E)L9Yb6d zLL8lZTou6L3L4p|l?o-P6(tI#d6|W!sX7W}i8){aO-&{?aa(alhRnQ_)QaN59ANH_ zPb@9T2lL~>+T#tu6ew@A@Ph1S0cP|*W(kNhM#<3-7!3hn2)v+qD70a*p$Ll@4TZQs eW`pwoC+0^qv}V-k(GVC7fq@DEIW&v;5PSg0W;EIW literal 0 HcmV?d00001 From 76d67d67f17513e07dab0b4c4579d4e12b599d5d Mon Sep 17 00:00:00 2001 From: Tom Galloway Date: Tue, 22 Jan 2013 20:22:19 +0000 Subject: [PATCH 24/24] Add User & Delete User now works correctly. --- modules/installed/system/users.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/modules/installed/system/users.py b/modules/installed/system/users.py index 63d9c7631..4277f1050 100644 --- a/modules/installed/system/users.py +++ b/modules/installed/system/users.py @@ -5,7 +5,7 @@ from plugin_mount import PagePlugin, FormPlugin import cfg from forms import Form from util import * -from pprint import pprint +from model import User class users(PagePlugin): order = 20 # order of running init in PagePlugins @@ -41,7 +41,7 @@ class add(FormPlugin, PagePlugin): form.text_input(_("Username"), name="username", value=username) form.text_input(_("Full name"), name="name", value=name) form.text_input(_("Email"), name="email", value=email) - form.text_input(_("Password"), name="password") + form.text_input(_("Password"), name="password", type="password") form.text_input(name="md5_password", type="hidden") form.submit(label=_("Create User"), name="create") return form.render() @@ -52,12 +52,12 @@ class add(FormPlugin, PagePlugin): if not username: msg.add = _("Must specify a username!") if not md5_password: msg.add = _("Must specify a password!") - if username in cfg.users.keys(): + if username in cfg.users.get_all(): msg.add = _("User already exists!") else: try: di = {'username':username, 'name':name, 'email':email, 'passphrase':md5_password} - new_user = User(dict=di) + new_user = User(di) cfg.users.set(username,new_user) except: msg.add = _("Error storing user!") @@ -65,7 +65,7 @@ class add(FormPlugin, PagePlugin): if not msg: msg.add = _("%s saved." % username) cfg.log(msg.text) - #main = self.make_form(username, name, email, msg=msg.text) + main = self.main(username, name, email, msg=msg.text) return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) class edit(FormPlugin, PagePlugin): @@ -80,14 +80,15 @@ class edit(FormPlugin, PagePlugin): system.

Deleting users is permanent!

""" % (cfg.product_name, cfg.box_name)) def main(self, msg=''): - users = cfg.users + users = cfg.users.get_all() add_form = Form(title=_("Edit or Delete User"), action="/sys/users/edit", message=msg) add_form.html('Delete
') - for uname in sorted(users.keys()): + for uname in users: + user = User(uname[1]) add_form.html('  %s     ' % - add_form.get_checkbox(name=uname) + + add_form.get_checkbox(name=user['username']) + '%s (%s)
' % - (uname, users[uname]['name'], uname)) + (user['username'], user['name'], user['username'])) add_form.submit(label=_("Delete User"), name="delete") return add_form.render() @@ -98,12 +99,12 @@ class edit(FormPlugin, PagePlugin): cfg.log.info("%s asked to delete %s" % (cherrypy.session.get(cfg.session_key), usernames)) if usernames: for username in usernames: - if username in cfg.users: + if cfg.users.exists(username): try: - del cfg.users[username] + cfg.users.remove(username) msg.add(_("Deleted user %s." % username)) except IOError, e: - if 'username' in cfg.users: + if cfg.users.exists(username): m = _("Error on deletion, user %s not fully deleted: %s" % (username, e)) cfg.log.error(m) msg.add(m) @@ -116,7 +117,7 @@ class edit(FormPlugin, PagePlugin): msg.add(_("User %s does not exist." % username)) else: msg.add = _("Must specify at least one valid, existing user.") - #main = self.make_form(msg=msg.text) + main = self.main(msg=msg.text) return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) sidebar_right = ''