Add mysql database stuff
authorMichael Spang <mspang@csclub.uwaterloo.ca>
Wed, 9 Sep 2009 21:37:35 +0000 (17:37 -0400)
committerMichael Spang <mspang@csclub.uwaterloo.ca>
Thu, 10 Sep 2009 11:33:32 +0000 (07:33 -0400)
18 files changed:
.gitignore
ceo/members.py
ceo/mysql.py [new file with mode: 0644]
ceo/urwid/databases.py
ceo/urwid/main.py
debian/control
debian/rules
etc/ops/adduser
etc/ops/mail
etc/ops/mysql [new file with mode: 0644]
src/Makefile
src/ceo.proto
src/dslave.c
src/op-mysql [new file with mode: 0755]
src/ops.c
src/ops.h
src/util.c
src/util.h

index 307fdb4..232247c 100644 (file)
@@ -1,3 +1,5 @@
 /build-stamp
 /build
 *.pyc
+/build-ceo
+/build-ceod
index 1154257..5ebf272 100644 (file)
@@ -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 (file)
index 0000000..f6b3e69
--- /dev/null
@@ -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)
+
index 40b89e4..a61120d 100644 (file)
@@ -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()
index 1fb1cd9..ba877f4 100644 (file)
@@ -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),
index df8b6b6..f2e41b1 100644 (file)
@@ -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.
index 6761d9a..90c0602 100755 (executable)
@@ -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
 
index 71ee80b..bbe019f 100644 (file)
@@ -1 +1 @@
-ginseng        adduser 0x01
+ginseng        adduser root 0x01
index 7666d8e..8a0152d 100644 (file)
@@ -1 +1 @@
-ginseng mail 0x02
+ginseng mail root 0x02
diff --git a/etc/ops/mysql b/etc/ops/mysql
new file mode 100644 (file)
index 0000000..d6bd10e
--- /dev/null
@@ -0,0 +1 @@
+caffeine mysql mysql 0x03
index d8f133b..aa8de8a 100644 (file)
@@ -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
 
index 701055c..4d803a0 100644 (file)
@@ -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;
+}
index 4ae7624..35c3d34 100644 (file)
@@ -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 (executable)
index 0000000..593005f
--- /dev/null
@@ -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()
index b86ef43..5055580 100644 (file)
--- a/src/ops.c
+++ b/src/ops.c
@@ -3,6 +3,7 @@
 #include <unistd.h>
 #include <fcntl.h>
 #include <netdb.h>
+#include <pwd.h>
 
 #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;
     }
index 3e7465c..69b5b5a 100644 (file)
--- 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);
index 3987ca4..e693b19 100644 (file)
@@ -8,6 +8,7 @@
 #include <syslog.h>
 #include <errno.h>
 #include <grp.h>
+#include <pwd.h>
 
 #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 {
index 8aedba8..c633902 100644 (file)
@@ -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);