diff --git a/.gitignore b/.gitignore index 307fdb4..232247c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /build-stamp /build *.pyc +/build-ceo +/build-ceod diff --git a/ceo/members.py b/ceo/members.py index 1154257..5ebf272 100644 --- a/ceo/members.py +++ b/ceo/members.py @@ -91,6 +91,11 @@ def connect(auth_callback): if password == None: raise e +def connect_anonymous(): + """Connect to LDAP.""" + + global ld + ld = ldap.initialize(cfg['ldap_server_url']) def disconnect(): """Disconnect from LDAP.""" diff --git a/ceo/mysql.py b/ceo/mysql.py new file mode 100644 index 0000000..f6b3e69 --- /dev/null +++ b/ceo/mysql.py @@ -0,0 +1,24 @@ +import os, re, subprocess, ldap, socket +from ceo import conf, ldapi, terms, remote, ceo_pb2 +from ceo.excep import InvalidArgument + +class MySQLException(Exception): + pass + +def create_mysql(username): + try: + request = ceo_pb2.AddMySQLUser() + request.username = username + + out = remote.run_remote('mysql', request.SerializeToString()) + + response = ceo_pb2.AddMySQLUserResponse() + response.ParseFromString(out) + + if any(message.status != 0 for message in response.messages): + raise MySQLException('\n'.join(message.message for message in response.messages)) + + return response.password + except remote.RemoteException, e: + raise MySQLException(e) + diff --git a/ceo/urwid/databases.py b/ceo/urwid/databases.py index 40b89e4..a61120d 100644 --- a/ceo/urwid/databases.py +++ b/ceo/urwid/databases.py @@ -1,15 +1,78 @@ import urwid -from ceo import members +from ceo import members, mysql from ceo.urwid import search from ceo.urwid.widgets import * from ceo.urwid.window import * -def databases(menu): - menu = make_menu([ - ("Create MySQL database", create_mysql_db, None), - ("Back", raise_back, None), - ]) - push_window(menu, "Databases") +class IntroPage(WizardPanel): + def init_widgets(self): + self.widgets = [ + urwid.Text("MySQL databases"), + urwid.Divider(), + urwid.Text("Members and hosted clubs may have one MySQL database each. You may " + "create a database for an account if: \n" + "\n" + "- It is your personal account,\n" + "- It is a club account, and you are in the club group, or\n" + "- You are on the CSC systems committee\n" + "\n" + "You may also use this to reset your database password." + ) + ] + def focusable(self): + return False -def create_mysql_db(data): - pass +class UserPage(WizardPanel): + def init_widgets(self): + self.userid = LdapWordEdit(csclub_uri, csclub_base, 'uid', + "Username: ") + + self.widgets = [ + urwid.Text("Member Information"), + urwid.Divider(), + urwid.Text("Enter the user which will own the new database."), + urwid.Divider(), + self.userid, + ] + def check(self): + self.state['userid'] = self.userid.get_edit_text() + self.state['member'] = None + if self.state['userid']: + self.state['member'] = members.get(self.userid.get_edit_text()) + if not self.state['member']: + set_status("Member not found") + self.focus_widget(self.userid) + return True + +class EndPage(WizardPanel): + def init_widgets(self): + self.headtext = urwid.Text("") + self.midtext = urwid.Text("") + + self.widgets = [ + self.headtext, + urwid.Divider(), + self.midtext, + ] + def focusable(self): + return False + def activate(self): + problem = None + try: + password = mysql.create_mysql(self.state['userid']) + self.headtext.set_text("MySQL database created") + self.midtext.set_text("Connection Information: \n" + "\n" + "Database: %s\n" + "Username: %s\n" + "Hostname: localhost\n" + "Password: %s\n" + "\n" + "Note: Databases are only accessible from caffeine\n" + % (self.state['userid'], self.state['userid'], password)) + except mysql.MySQLException, e: + self.headtext.set_text("Failed to create MySQL database") + self.midtext.set_text("We failed to create the database. The error was:\n\n%s" % e) + + def check(self): + pop_window() diff --git a/ceo/urwid/main.py b/ceo/urwid/main.py index 1fb1cd9..ba877f4 100644 --- a/ceo/urwid/main.py +++ b/ceo/urwid/main.py @@ -143,6 +143,13 @@ def change_shell(data): shell.EndPage ], (50, 20)) +def create_mysql_db(data): + push_wizard("Create MySQL database", [ + databases.IntroPage, + databases.UserPage, + databases.EndPage, + ], (60, 15)) + def check_group(group): try: me = pwd.getpwuid(os.getuid()).pw_name @@ -158,7 +165,6 @@ def top_menu(): ("Renew Club Rep", renew_club_user, None), ("New Club", new_club, None), ("Library", library.library, None), - ("Databases", databases.databases, None), ] syscom_only = [ ("Manage Club or Group Members", manage_group, None), @@ -168,8 +174,9 @@ def top_menu(): ] unrestricted = [ ("Display Member", display_member, None), - ("Change Shell", change_shell, None), ("Search", search_members, None), + ("Change Shell", change_shell, None), + ("Create MySQL database", create_mysql_db, None), ] footer = [ ("Exit", raise_abort, None), diff --git a/debian/control b/debian/control index df8b6b6..f2e41b1 100644 --- a/debian/control +++ b/debian/control @@ -29,7 +29,7 @@ Description: Computer Science Club Administrative Clients Package: ceo-daemon Architecture: any -Depends: ceo-python, ${shlibs:Depends} +Depends: ceo-python, ${python:Depends}, ${shlibs:Depends} Description: Computer Science Club Administrative Daemon This package contains the CSC Electronic Office daemon. diff --git a/debian/rules b/debian/rules index 6761d9a..90c0602 100755 --- a/debian/rules +++ b/debian/rules @@ -4,7 +4,6 @@ CFLAGS := -g -O2 -fstack-protector-all -fPIE LDFLAGS := -pie -Wl,--as-needed build: - python setup.py -q build cd src && make CFLAGS="$(CFLAGS)" LDFLAGS="$(LDFLAGS)" clean: @@ -12,13 +11,17 @@ clean: dh_testroot dh_clean $(MAKE) -C src clean - python setup.py -q clean -a + python setup.py -q clean -a --build-base=build-ceo + python setupd.py -q clean -a --build-base=build-ceod + rm -rf build-ceo build-ceod install: build dh_testdir dh_testroot dh_installdirs - python setup.py -q install --no-compile -O0 --prefix=/usr --root=debian/ceo-python + python setup.py -q build --build-base=build-ceo install --no-compile -O0 --prefix=/usr --root=debian/ceo-python + python setupd.py -q build --build-base=build-ceod install --no-compile -O0 --prefix=/usr --root=debian/ceo-daemon \ + --install-scripts=/usr/lib/ceod $(MAKE) -C src DESTDIR=$(CURDIR)/debian/ceo-clients PREFIX=/usr install_clients $(MAKE) -C src DESTDIR=$(CURDIR)/debian/ceo-daemon PREFIX=/usr install_daemon diff --git a/etc/ops/adduser b/etc/ops/adduser index 71ee80b..bbe019f 100644 --- a/etc/ops/adduser +++ b/etc/ops/adduser @@ -1 +1 @@ -ginseng adduser 0x01 +ginseng adduser root 0x01 diff --git a/etc/ops/mail b/etc/ops/mail index 7666d8e..8a0152d 100644 --- a/etc/ops/mail +++ b/etc/ops/mail @@ -1 +1 @@ -ginseng mail 0x02 +ginseng mail root 0x02 diff --git a/etc/ops/mysql b/etc/ops/mysql new file mode 100644 index 0000000..d6bd10e --- /dev/null +++ b/etc/ops/mysql @@ -0,0 +1 @@ +caffeine mysql mysql 0x03 diff --git a/src/Makefile b/src/Makefile index d8f133b..aa8de8a 100644 --- a/src/Makefile +++ b/src/Makefile @@ -35,6 +35,7 @@ all: $(BIN_PROGS) $(LIB_PROGS) $(EXT_PROGS) ../ceo/ceo_pb2.py clean: rm -f $(BIN_PROGS) $(LIB_PROGS) $(EXT_PROGS) *.o ceo.pb-c.c ceo.pb-c.h + rm -f ceo_pb2.py ../ceo/ceo_pb2.py op-adduser.o addmember.o addclub.o: ceo.pb-c.h diff --git a/src/ceo.proto b/src/ceo.proto index 701055c..4d803a0 100644 --- a/src/ceo.proto +++ b/src/ceo.proto @@ -31,3 +31,12 @@ message UpdateMail { message UpdateMailResponse { repeated StatusMessage messages = 1; } + +message AddMySQLUser { + required string username = 1; +} + +message AddMySQLUserResponse { + repeated StatusMessage messages = 1; + optional string password = 2; +} diff --git a/src/dslave.c b/src/dslave.c index 4ae7624..35c3d34 100644 --- a/src/dslave.c +++ b/src/dslave.c @@ -97,7 +97,7 @@ static void handle_op_message(uint32_t in_type, struct strbuf *in, struct strbuf "CEO_CONFIG_DIR", config_dir, NULL); char *argv[] = { op->path, NULL, }; - if (spawnvem(op->path, argv, envp, in, out, 0)) + if (spawnvemu(op->path, argv, envp, in, out, 0, op->user)) fatal("child %s failed", op->path); if (!out->len) diff --git a/src/op-mysql b/src/op-mysql new file mode 100755 index 0000000..593005f --- /dev/null +++ b/src/op-mysql @@ -0,0 +1,113 @@ +#!/usr/bin/python + +import os, sys, string, random, syslog, grp, errno, re +from ceo import ceo_pb2, members, conf +import MySQLdb + +CONFIG_FILE = '/etc/csc/mysql.cf' + +cfg = {} + +def configure(): + string_fields = ['mysql_admin_username', 'mysql_admin_password'] + + # read configuration file + cfg_tmp = conf.read(CONFIG_FILE) + + # verify configuration + conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp) + + # update the current configuration with the loaded values + cfg.update(cfg_tmp) + +def response_message(response, status, message): + priority = syslog.LOG_ERR if status else syslog.LOG_INFO + syslog.syslog(priority, message) + msg = response.messages.add() + msg.status = status + msg.message = message + return status + +def random_password(): + chars = string.letters + string.digits + return ''.join(random.choice(chars) for i in xrange(20)) + +def get_ceo_user(): + user = os.environ.get('CEO_USER') + if not user: + raise Exception("environment variable CEO_USER not set"); + return user + +def check_group(user, group): + try: + return user in grp.getgrnam(group).gr_mem + except KeyError: + return False + +def check_auth(remote_user, mysql_user, response): + if remote_user == mysql_user: + return response_message(response, 0, 'user %s creating database for self' % remote_user) + club = members.get(mysql_user) + if 'club' in club.get('objectClass', []): + if check_group(remote_user, mysql_user): + return response_message(response, 0, 'user %s is in club group %s' % (remote_user, mysql_user)) + else: + return response_message(response, errno.EPERM, 'denied, user %s is not in club group %s' % (remote_user, mysql_user)) + if check_group(remote_user, 'syscom'): + return response_message(response, 0, 'user %s is on systems committee' % remote_user) + else: + return response_message(response, errno.EPERM, 'denied, you may not create databases for other members') + +def mysql_createdb(remote_user, mysql_user, response): + if check_auth(remote_user, mysql_user, response): + return + + response.password = random_password() + + if not re.match('^[a-zA-Z0-9-]+$', mysql_user): + response_message(response, errno.EINVAL, 'invalid characters in username %s' % mysql_user) + return + + if not re.match('^[a-zA-Z0-9-]+$', response.password): + response_message(response, errno.EINVAL, 'invalid characters in password %s' % response.password) + return + + try: + connection = MySQLdb.Connect(user=cfg['mysql_admin_username'], passwd=cfg['mysql_admin_password']) + cursor = connection.cursor() + cursor.execute("GRANT ALL PRIVILEGES ON `%s`.* TO `%s`@`localhost` IDENTIFIED BY '%s'" + % (mysql_user, mysql_user, response.password)) + cursor.execute("CREATE DATABASE IF NOT EXISTS `%s`" % mysql_user) + cursor.close() + connection.close() + + response_message(response, 0, 'successfully created database %s' % mysql_user) + except MySQLdb.MySQLError, e: + response_message(response, 1, 'exception occured creating database: %s' % e) + + +def mysql_op(): + input = sys.stdin.read() + + request = ceo_pb2.AddMySQLUser() + request.ParseFromString(input) + + remote_user = get_ceo_user() + mysql_user = request.username + + response = ceo_pb2.AddMySQLUserResponse() + response_message(response, 0, 'mysql create db=%s by %s' % (mysql_user, remote_user)) + + mysql_createdb(remote_user, mysql_user, response) + + sys.stdout.write(response.SerializeToString()) + +def main(): + configure() + members.configure() + members.connect_anonymous() + syslog.openlog('op-mysql', syslog.LOG_PID, syslog.LOG_DAEMON) + mysql_op() + +if __name__ == '__main__': + main() diff --git a/src/ops.c b/src/ops.c index b86ef43..5055580 100644 --- a/src/ops.c +++ b/src/ops.c @@ -3,6 +3,7 @@ #include #include #include +#include #include "strbuf.h" #include "ops.h" @@ -15,13 +16,14 @@ static struct op *ops; static const char *default_op_dir = "/usr/lib/ceod"; static const char *op_dir; -static void add_op(char *host, char *name, uint32_t id) { +static void add_op(char *host, char *name, char *user, uint32_t id) { struct op *new = xmalloc(sizeof(struct op)); errno = 0; new->next = ops; new->name = xstrdup(name); new->id = id; new->path = NULL; + new->user = xstrdup(user); struct hostent *hostent = gethostbyname(host); if (!hostent) @@ -35,11 +37,15 @@ static void add_op(char *host, char *name, uint32_t id) { sprintf(new->path, "%s/op-%s", op_dir, name); if (access(new->path, X_OK)) fatalpe("cannot add op: %s: %s", name, new->path); + + struct passwd *pw = getpwnam(user); + if (!pw) + fatalpe("cannot add op %s: getpwnam: %s", name, user); } ops = new; - debug("added op %s (%s%s)", new->name, new->local ? "" : "on ", - new->local ? "local" : host); + debug("added op %s (%s%s) [%s]", new->name, new->local ? "" : "on ", + new->local ? "local" : host, new->user); } struct op *get_local_op(uint32_t id) { @@ -88,16 +94,16 @@ void setup_ops(void) { struct strbuf **words = strbuf_splitws(&line); - if (strbuf_list_len(words) != 3) - badconf("%s/%s: expected three words on line %d", op_config_dir, de->d_name, lineno); + if (strbuf_list_len(words) != 4) + badconf("%s/%s: expected four words on line %d", op_config_dir, de->d_name, lineno); errno = 0; char *end; - int id = strtol(words[2]->buf, &end, 0); + int id = strtol(words[3]->buf, &end, 0); if (errno || *end) badconf("%s/%s: invalid id '%s' on line %d", op_config_dir, de->d_name, words[2]->buf, lineno); - add_op(words[0]->buf, words[1]->buf, id); + add_op(words[0]->buf, words[1]->buf, words[2]->buf, id); op_count++; strbuf_list_free(words); @@ -115,6 +121,7 @@ void free_ops(void) { free(ops->name); free(ops->hostname); free(ops->path); + free(ops->user); free(ops); ops = next; } diff --git a/src/ops.h b/src/ops.h index 3e7465c..69b5b5a 100644 --- a/src/ops.h +++ b/src/ops.h @@ -6,6 +6,7 @@ struct op { char *path; struct in_addr addr; struct op *next; + char *user; }; void setup_ops(void); diff --git a/src/util.c b/src/util.c index 3987ca4..e693b19 100644 --- a/src/util.c +++ b/src/util.c @@ -8,6 +8,7 @@ #include #include #include +#include #include "util.h" #include "strbuf.h" @@ -173,6 +174,10 @@ void full_write(int fd, const void *buf, size_t count) { } int spawnvem(const char *path, char *const *argv, char *const *envp, const struct strbuf *output, struct strbuf *input, int cap_stderr) { + return spawnvemu(path, argv, envp, output, input, cap_stderr, NULL); +} + +int spawnvemu(const char *path, char *const *argv, char *const *envp, const struct strbuf *output, struct strbuf *input, int cap_stderr, char *user) { int pid, wpid, status; int tochild[2]; int fmchild[2]; @@ -197,6 +202,18 @@ int spawnvem(const char *path, char *const *argv, char *const *envp, const struc close(tochild[1]); close(fmchild[0]); close(fmchild[1]); + + if (user) { + struct passwd *pw = getpwnam(user); + if (!pw) + fatalpe("getpwnam: %s", user); + if (initgroups(user, pw->pw_gid)) + fatalpe("initgroups: %s", user); + if (setregid(pw->pw_gid, pw->pw_gid)) + fatalpe("setregid: %s", user); + if (setreuid(pw->pw_uid, pw->pw_uid)) + fatalpe("setreuid"); + } execve(path, argv, envp); fatalpe("execve"); } else { diff --git a/src/util.h b/src/util.h index 8aedba8..c633902 100644 --- a/src/util.h +++ b/src/util.h @@ -28,6 +28,7 @@ extern char **environ; int spawnv(const char *path, char *const *argv); int spawnv_msg(const char *path, char *const *argv, const struct strbuf *output); int spawnvem(const char *path, char *const *argv, char *const *envp, const struct strbuf *output, struct strbuf *input, int cap_stderr); +int spawnvemu(const char *path, char *const *argv, char *const *envp, const struct strbuf *output, struct strbuf *input, int cap_stderr, char *user); void full_write(int fd, const void *buf, size_t count); ssize_t full_read(int fd, void *buf, size_t len); FILE *fopenat(DIR *d, const char *path, int flags);