From 3d20172dad4dee0c8f9e10a972d0c6fb4eb2e786 Mon Sep 17 00:00:00 2001 From: Michael Spang Date: Mon, 10 Dec 2007 00:25:14 -0500 Subject: [PATCH] Add sources for C account creation programs --- src/.gitignore | 4 + src/Makefile | 20 ++++ src/addclub.c | 181 +++++++++++++++++++++++++++++ src/addhomedir.c | 133 +++++++++++++++++++++ src/addhomedir.h | 4 + src/addmember.c | 196 +++++++++++++++++++++++++++++++ src/common.c | 58 ++++++++++ src/common.h | 3 + src/config-test.c | 21 ++++ src/config.c | 96 ++++++++++++++++ src/config.h | 42 +++++++ src/kadm.c | 66 +++++++++++ src/kadm.h | 5 + src/krb5.c | 149 ++++++++++++++++++++++++ src/krb5.h | 11 ++ src/ldap.c | 286 ++++++++++++++++++++++++++++++++++++++++++++++ src/ldap.h | 10 ++ src/parser.c | 214 ++++++++++++++++++++++++++++++++++ src/parser.h | 2 + src/util.c | 136 ++++++++++++++++++++++ src/util.h | 44 +++++++ 21 files changed, 1681 insertions(+) create mode 100644 src/.gitignore create mode 100644 src/Makefile create mode 100644 src/addclub.c create mode 100644 src/addhomedir.c create mode 100644 src/addhomedir.h create mode 100644 src/addmember.c create mode 100644 src/common.c create mode 100644 src/common.h create mode 100644 src/config-test.c create mode 100644 src/config.c create mode 100644 src/config.h create mode 100644 src/kadm.c create mode 100644 src/kadm.h create mode 100644 src/krb5.c create mode 100644 src/krb5.h create mode 100644 src/ldap.c create mode 100644 src/ldap.h create mode 100644 src/parser.c create mode 100644 src/parser.h create mode 100644 src/util.c create mode 100644 src/util.h diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..d02cf79 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,4 @@ +*.o +/addmember +/addclub +/config-test diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..ac13cd6 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,20 @@ +CFLAGS ?= -ggdb -Wall -O2 +CFLAGS += -I../include +LDAP := -lldap +KADM := $(shell krb5-config --libs krb5 kadm-client) + +LIBCEO := util.o common.o config.o parser.o ldap.o krb5.o kadm.o addhomedir.o + +all: addmember addclub + +clean: + rm -f addmember addclub config-test *.o + +addmember: $(LIBCEO) addmember.o + $(CC) $(LDFLAGS) $(LDAP) $(KADM) $^ -o $@ + +addclub: $(LIBCEO) addclub.o + $(CC) $(LDFLAGS) $(LDAP) $(KADM) $^ -o $@ + +config-test: config-test.o parser.o util.o + $(CC) $(LDFLAGS) $^ -o $@ diff --git a/src/addclub.c b/src/addclub.c new file mode 100644 index 0000000..6bae715 --- /dev/null +++ b/src/addclub.c @@ -0,0 +1,181 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" +#include "common.h" +#include "config.h" +#include "ldap.h" +#include "krb5.h" +#include "kadm.h" +#include "addhomedir.h" + +char *prog = NULL; +char *user = NULL; +int privileged = 0; + +static int force = 0; +static int no_notify = 0; + +static char *name = NULL; +static char *userid = NULL; + +static struct option opts[] = { + { "force", 0, NULL, 'f' }, + { "no-notify", 0, NULL, 'q' }, + { NULL, 0, NULL, '\0' }, +}; + +static void usage() { + fprintf(stderr, "Usage: %s userid clubname\n", prog); + exit(2); +} + +void addclub() { + int krb_ok, user_ok, group_ok, home_ok, quota_ok; + int id; + char homedir[1024]; + + logmsg("adding uid=%s cn=%s by %s", userid, name, user); + + if (setreuid(0, 0)) + fatalpe("setreuid"); + + if (!force && getpwnam(userid) != NULL) + deny("user %s already exists", userid); + + ceo_krb5_init(); + ceo_ldap_init(); + ceo_kadm_init(); + + if (ceo_user_exists(userid)) + deny("user %s already exists in LDAP", userid); + + if ((id = ceo_new_uid(member_min_id, member_max_id)) <= 0) + fatal("no available uids in range [%d, %d]", member_min_id, member_max_id); + + snprintf(homedir, sizeof(homedir), "%s/%s", club_home, userid); + + krb_ok = ceo_del_princ(userid); + if (!krb_ok) + logmsg("successfully cleared principal for %s", userid); + + user_ok = krb_ok || ceo_add_user(userid, users_base, "club", name, homedir, + club_shell, id, NULL); + if (!user_ok) + logmsg("successfully created account for %s", userid); + + group_ok = user_ok || ceo_add_group(userid, groups_base, id); + if (!group_ok) + logmsg("successfully created group for %s", userid); + + home_ok = user_ok || ceo_create_home(homedir, id, id); + if (!home_ok) + logmsg("successfully created home directory for %s", userid); + + quota_ok = user_ok || ceo_set_quota(quota_prototype, id); + if (!quota_ok) + logmsg("successfully set quota for %s", userid); + + logmsg("done uid=%s", userid); + + if (!no_notify && !user_ok) { + int pid; + int hkp[2]; + FILE *hkf; + int status; + + if (pipe(hkp)) + errorpe("pipe"); + + fflush(stdout); + fflush(stderr); + + pid = fork(); + + if (!pid) { + fclose(stdout); + fclose(stderr); + close(hkp[1]); + dup2(hkp[0], 0); + exit(execl(notify_hook, notify_hook, prog, user, userid, name, NULL)); + } + + hkf = fdopen(hkp[1], "w"); + + if (group_ok) + fprintf(hkf, "failed to create group\n"); + if (home_ok) + fprintf(hkf, "failed to create home directory\n"); + if (quota_ok) + fprintf(hkf, "failed to set quota\n"); + if (!group_ok && !home_ok && !quota_ok) + fprintf(hkf, "all failures went undetected\n"); + + fclose(hkf); + + waitpid(pid, &status, 0); + + if (WIFEXITED(status) && WEXITSTATUS(status)) + logmsg("hook %s exited with status %d", notify_hook, WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + logmsg("hook %s killed by signal %d", notify_hook, WTERMSIG(status)); + } + + ceo_kadm_cleanup(); + ceo_ldap_cleanup(); + ceo_krb5_cleanup(); + + return !krb_ok && !user_ok && !group_ok && !home_ok && quota_ok; +} + +int main(int argc, char *argv[]) { + int opt; + + openlog(prog, 0, LOG_AUTHPRIV); + + configure(); + + prog = basename(argv[0]); + user = ceo_get_user(); + privileged = ceo_get_privileged(); + + while ((opt = getopt_long(argc, argv, "", opts, NULL)) != -1) { + switch (opt) { + case 'f': + if (!privileged) + deny("not privileged enough to force"); + force = 1; + break; + case 'q': + if (!privileged) + deny("not privileged enough to suppress notifications"); + no_notify = 1; + break; + case '?': + usage(); + break; + default: + fatal("error parsing arguments"); + } + } + + if (argc - optind != 2) + usage(); + + userid = argv[optind++]; + name = argv[optind++]; + + addclub(); + + return 0; +} diff --git a/src/addhomedir.c b/src/addhomedir.c new file mode 100644 index 0000000..c2ad7fa --- /dev/null +++ b/src/addhomedir.c @@ -0,0 +1,133 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "addhomedir.h" +#include "util.h" +#include "config.h" + +int ceo_create_home(char *homedir, uid_t uid, gid_t gid) { + int mask; + DIR *skel; + struct dirent *skelent; + + mask = umask(0); + + if (mkdir(homedir, homedir_mode)) { + errorpe("failed to create %s", homedir); + return -1; + } + + skel = opendir(skeleton_dir); + if (!skel) { + errorpe("failed to open %s", skeleton_dir); + return -1; + } + + while ((skelent = readdir(skel))) { + struct stat sb; + char src[PATH_MAX], dest[PATH_MAX]; + + if (!strcmp(skelent->d_name, ".") || !strcmp(skelent->d_name, "..")) + continue; + + snprintf(src, sizeof(src), "%s/%s", skeleton_dir, skelent->d_name); + snprintf(dest, sizeof(dest), "%s/%s", homedir, skelent->d_name); + lstat(src, &sb); + + if (sb.st_uid || sb.st_gid) { + warn("not creating %s due to ownership", dest); + continue; + } + + if (S_ISREG(sb.st_mode)) { + int bytes; + char buf[4096]; + + int srcfd = open(src, O_RDONLY); + if (srcfd == -1) { + warnpe("open: %s", src); + continue; + } + + int destfd = open(dest, O_WRONLY|O_CREAT|O_EXCL, sb.st_mode & 0777); + if (destfd == -1) { + warnpe("open: %s", dest); + close(srcfd); + continue; + } + + for (;;) { + bytes = read(srcfd, buf, sizeof(buf)); + if (!bytes) + break; + if (bytes < 0) { + warnpe("read"); + break; + } + if (write(destfd, buf, bytes) < 0) { + warnpe("write"); + break; + } + } + + if (fchown(destfd, uid, gid)) + errorpe("chown: %s", dest); + + close(srcfd); + close(destfd); + } else if (S_ISDIR(sb.st_mode)) { + if (mkdir(dest, sb.st_mode & 0777)) { + warnpe("mkdir: %s", dest); + continue; + } + if (chown(dest, uid, gid)) + errorpe("chown: %s", dest); + } else if (S_ISLNK(sb.st_mode)) { + char lnkdest[PATH_MAX]; + int bytes; + bytes = readlink(src, lnkdest, sizeof(lnkdest)); + lnkdest[bytes] = '\0'; + if (bytes == -1) { + warnpe("readlink: %s", src); + continue; + } + if (symlink(lnkdest, dest)) { + warnpe("symlink: %s", dest); + continue; + } + if (lchown(dest, uid, gid)) + errorpe("lchown: %s", dest); + } else { + warn("not creating %s", dest); + } + } + + closedir(skel); + + if (chown(homedir, uid, gid)) + errorpe("failed to chown %s", homedir); + + umask(mask); + + return 0; +} + +int ceo_set_quota(char *proto, int id) { + char user[128]; + char *sqargs[] = { "setquota", "-a", "-p", proto, NULL, NULL }; + + snprintf(user, sizeof(user), "%d", id); + sqargs[4] = user; + + if (spawnv("/usr/sbin/setquota", sqargs)) { + error("failed to set quota for %s", user); + return -1; + } + + return 0; +} diff --git a/src/addhomedir.h b/src/addhomedir.h new file mode 100644 index 0000000..50f7062 --- /dev/null +++ b/src/addhomedir.h @@ -0,0 +1,4 @@ +#include + +int ceo_create_home(char *, uid_t, gid_t); +int ceo_set_quota(char *, int); diff --git a/src/addmember.c b/src/addmember.c new file mode 100644 index 0000000..382e19f --- /dev/null +++ b/src/addmember.c @@ -0,0 +1,196 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" +#include "common.h" +#include "config.h" +#include "ldap.h" +#include "krb5.h" +#include "kadm.h" +#include "addhomedir.h" + +char *prog = NULL; +char *user = NULL; +int privileged = 0; + +static int force = 0; +static int no_notify = 0; + +static int use_stdin = 0; + +static char *name = NULL; +static char *userid = NULL; +static char *program = NULL; +static char password[1024]; + +static struct option opts[] = { + { "force", 0, NULL, 'f' }, + { "no-notify", 0, NULL, 'q' }, + { "stdin", 0, NULL, 's' }, + { NULL, 0, NULL, '\0' }, +}; + +static void usage() { + fprintf(stderr, "Usage: %s userid realname [program]\n", prog); + exit(2); +} + +void addmember() { + int krb_ok, user_ok, group_ok, home_ok, quota_ok; + int id; + char homedir[1024]; + + logmsg("adding uid=%s cn=%s program=%s by %s", userid, name, program, user); + + if (setreuid(0, 0)) + fatalpe("setreuid"); + + if (!force && getpwnam(userid) != NULL) + deny("user %s already exists", userid); + + if (ceo_read_password(password, sizeof(password), use_stdin)) + return; + + ceo_krb5_init(); + ceo_ldap_init(); + ceo_kadm_init(); + + if (ceo_user_exists(userid)) + deny("user %s already exists in LDAP", userid); + + if ((id = ceo_new_uid(member_min_id, member_max_id)) <= 0) + fatal("no available uids in range [%d, %d]", member_min_id, member_max_id); + + snprintf(homedir, sizeof(homedir), "%s/%s", member_home, userid); + + krb_ok = ceo_del_princ(userid); + krb_ok = krb_ok || ceo_add_princ(userid, password); + if (!krb_ok) + logmsg("successfully created principal for %s", userid); + + user_ok = krb_ok || ceo_add_user(userid, users_base, "member", name, homedir, + member_shell, id, "program", program, NULL); + if (!user_ok) + logmsg("successfully created account for %s", userid); + + group_ok = user_ok || ceo_add_group(userid, groups_base, id); + if (!group_ok) + logmsg("successfully created group for %s", userid); + + home_ok = user_ok || ceo_create_home(homedir, id, id); + if (!home_ok) + logmsg("successfully created home directory for %s", userid); + + quota_ok = user_ok || ceo_set_quota(quota_prototype, id); + if (!quota_ok) + logmsg("successfully set quota for %s", userid); + + logmsg("done uid=%s", userid); + + if (!no_notify && !user_ok) { + int pid; + int hkp[2]; + FILE *hkf; + int status; + + if (pipe(hkp)) + errorpe("pipe"); + + fflush(stdout); + fflush(stderr); + + pid = fork(); + + if (!pid) { + fclose(stdout); + fclose(stderr); + close(hkp[1]); + dup2(hkp[0], 0); + exit(execl(notify_hook, notify_hook, prog, user, userid, name, program, NULL)); + } + + hkf = fdopen(hkp[1], "w"); + + if (group_ok) + fprintf(hkf, "failed to create group\n"); + if (home_ok) + fprintf(hkf, "failed to create home directory\n"); + if (quota_ok) + fprintf(hkf, "failed to set quota\n"); + if (!group_ok && !home_ok && !quota_ok) + fprintf(hkf, "all failures went undetected\n"); + + fclose(hkf); + + waitpid(pid, &status, 0); + + if (WIFEXITED(status) && WEXITSTATUS(status)) + logmsg("hook %s exited with status %d", notify_hook, WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + logmsg("hook %s killed by signal %d", notify_hook, WTERMSIG(status)); + } + + ceo_kadm_cleanup(); + ceo_ldap_cleanup(); + ceo_krb5_cleanup(); + + return !krb_ok && !user_ok && !group_ok && !home_ok && quota_ok; +} + +int main(int argc, char *argv[]) { + int opt; + + openlog(prog, 0, LOG_AUTHPRIV); + + configure(); + + prog = basename(argv[0]); + user = ceo_get_user(); + privileged = ceo_get_privileged(); + + while ((opt = getopt_long(argc, argv, "", opts, NULL)) != -1) { + switch (opt) { + case 's': + use_stdin = 1; + break; + case 'f': + if (!privileged) + deny("not privileged enough to force"); + force = 1; + break; + case 'q': + if (!privileged) + deny("not privileged enough to suppress notifications"); + no_notify = 1; + break; + case '?': + usage(); + break; + default: + fatal("error parsing arguments"); + } + } + + if (argc - optind != 2 && argc - optind != 3) + usage(); + + userid = argv[optind++]; + name = argv[optind++]; + + if (argc - optind) + program = argv[optind++]; + + addmember(); + + return 0; +} diff --git a/src/common.c b/src/common.c new file mode 100644 index 0000000..24edc90 --- /dev/null +++ b/src/common.c @@ -0,0 +1,58 @@ +#include +#include +#include +#include + +#include "common.h" +#include "util.h" +#include "config.h" + +int ceo_get_privileged() { + int uid = getuid(); + + // root is privileged + if (!uid) + return 1; + + if (privileged_group) { + struct group *privgrp = getgrnam(privileged_group); + int pgid; + gid_t grps[128]; + int count, i; + if (!privgrp) + return 0; + pgid = privgrp->gr_gid; + + count = getgroups(sizeof(grps), grps); + for (i = 0; i < count; i++) + if (grps[i] == pgid) + return 1; + } + + return 0; +} + +char *ceo_get_user() { + struct passwd *pwent = getpwuid(getuid()); + if (pwent == NULL) + fatal("could not determine user"); + return xstrdup(pwent->pw_name); +} + +void ceo_notify_hook(int argc, ...) { + va_list args; + char **argv; + int i = 0; + + va_start(args, argc); + + argv = (char **)xmalloc(sizeof(char *) * (argc + 1)); + + while (i < argc) + argv[i++] = va_arg(args, char *); + + argv[i++] = NULL; + spawnv(notify_hook, argv); + + va_end(args); +} diff --git a/src/common.h b/src/common.h new file mode 100644 index 0000000..cd17722 --- /dev/null +++ b/src/common.h @@ -0,0 +1,3 @@ +int ceo_get_privileged(); +char *ceo_get_user(); +void ceo_notify_hook(int, ...); diff --git a/src/config-test.c b/src/config-test.c new file mode 100644 index 0000000..5e0f0d9 --- /dev/null +++ b/src/config-test.c @@ -0,0 +1,21 @@ +#include +#include +#include + +#include "parser.h" +#include "util.h" + +void config_var(const char *name, const char *value) { + printf("%s = \"%s\"\n", name, value); +} + +int main(int argc, char *argv[]) { + if (argc < 2) { + fprintf(stderr, "usage: %s filename\n\n", argv[0]); + exit(1); + } + + config_parse(argv[1]); + + return 0; +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..13b4219 --- /dev/null +++ b/src/config.c @@ -0,0 +1,96 @@ +#include +#include + +#include "config.h" +#include "parser.h" +#include "util.h" + +#define DEF_STR NULL +#define DEF_LONG LONG_MIN + +char *server_url = DEF_STR; + +char *users_base = DEF_STR; +char *groups_base = DEF_STR; + +char *skeleton_dir = DEF_STR; +char *quota_prototype = DEF_STR; +char *homedir_prefix = DEF_STR; + +char *member_home = DEF_STR; +char *member_shell = DEF_STR; +long member_min_id = DEF_LONG; +long member_max_id = DEF_LONG; + +char *club_home = DEF_STR; +char *club_shell = DEF_STR; +long club_min_id = DEF_LONG; +long club_max_id = DEF_LONG; + +char *notify_hook = DEF_STR; + +char *realm = DEF_STR; + +char *admin_principal = DEF_STR; +char *admin_keytab = DEF_STR; + +char *admin_bind_userid = DEF_STR; +char *admin_bind_keytab = DEF_STR; + +char *sasl_realm = DEF_STR; +char *sasl_mech = DEF_STR; + +char *privileged_group = DEF_STR; + +long homedir_mode = DEF_LONG; +long homedir_min_uid = DEF_LONG; + +static char *strvarnames[] = { "server_url", "users_base", "admin_principal", + "admin_keytab", "skeleton_dir", "quota_prototype", "homedir_prefix", + "member_home", "member_shell", "club_home", "club_shell", "realm", + "admin_bind_userid", "admin_bind_keytab", "groups_base", + "privileged_group", "notify_hook", "sasl_realm", "sasl_mech" }; +static char **strvars[] = { &server_url, &users_base, &admin_principal, + &admin_keytab, &skeleton_dir, "a_prototype, &homedir_prefix, + &member_home, &member_shell, &club_home, &club_shell, &realm, + &admin_bind_userid, &admin_bind_keytab, &groups_base, + &privileged_group, ¬ify_hook, &sasl_realm, &sasl_mech }; + +static char *longvarnames[] = { "member_min_id", "member_max_id", + "homedir_mode", "homedir_min_uid", "club_min_id", "club_max_id" }; +static long *longvars[] = { &member_min_id, &member_max_id, &homedir_mode, + &homedir_min_uid, &club_min_id, &club_max_id }; + +void config_var(char *var, char *val) { + int i; + + for (i = 0; i < sizeof(strvars)/sizeof(*strvars); i++) { + if (!strcmp(var, strvarnames[i])) { + if (!strvars[i]) + free(strvars[i]); + *strvars[i] = xstrdup(val); + } + } + + for (i = 0; i < sizeof(longvars)/sizeof(*longvars); i++) { + if (!strcmp(var, longvarnames[i])) { + *longvars[i] = config_long(var, val); + } + } +} + +void configure() { + int i; + + config_parse(CONFIG_FILE); + + for (i = 0; i < sizeof(strvars)/sizeof(*strvars); i++) { + if (*strvars[i] == DEF_STR) + badconf("undefined string variable: %s", strvarnames[i]); + } + + for (i = 0; i < sizeof(longvars)/sizeof(*longvars); i++) { + if (*longvars[i] == DEF_LONG) + badconf("undefined long variable: %s", longvarnames[i]); + } +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..3cce5ea --- /dev/null +++ b/src/config.h @@ -0,0 +1,42 @@ +#define CONFIG_FILE "/etc/csc/accounts.cf" + +extern char *server_url; +extern char *users_base; +extern char *groups_base; + +extern char *admin_bind_dn; +extern char *admin_bind_pw; + +extern char *skeleton_dir; +extern char *quota_prototype; +extern char *homedir_prefix; + +extern char *member_home; +extern char *member_shell; +extern long member_min_id; +extern long member_max_id; + +extern char *club_home; +extern char *club_shell; +extern long club_min_id; +extern long club_max_id; + +extern char *notify_hook; + +extern long homedir_mode; +extern long homedir_min_uid; + +extern char *realm; + +extern char *admin_principal; +extern char *admin_keytab; + +extern char *admin_bind_userid; +extern char *admin_bind_keytab; + +extern char *sasl_realm; +extern char *sasl_mech; + +extern char *privileged_group; + +void configure(); diff --git a/src/kadm.c b/src/kadm.c new file mode 100644 index 0000000..0fd05b6 --- /dev/null +++ b/src/kadm.c @@ -0,0 +1,66 @@ +#include + +#include "kadm.h" +#include "krb5.h" +#include "util.h" +#include "config.h" + +extern char *prog; + +static void *handle; + +void ceo_kadm_init() { + krb5_error_code retval; + kadm5_config_params params; + memset((void *) ¶ms, 0, sizeof(params)); + + retval = kadm5_init_with_skey(admin_principal, admin_keytab, + KADM5_ADMIN_SERVICE, ¶ms, KADM5_STRUCT_VERSION, + KADM5_API_VERSION_2, &handle); + if (retval) { + com_err(prog, retval, "while initializing kadm5"); + exit(1); + } +} + +void ceo_kadm_cleanup() { + kadm5_destroy(handle); +} + +int ceo_add_princ(char *user, char *password) { + krb5_error_code retval; + kadm5_principal_ent_rec princ; + memset((void *) &princ, 0, sizeof(princ)); + + if ((retval = krb5_parse_name(context, user, &princ.principal))) { + com_err(prog, retval, "while parsing principal name"); + return retval; + } + + if ((retval = kadm5_create_principal(handle, &princ, KADM5_PRINCIPAL, password))) { + com_err(prog, retval, "while creating principal"); + return retval; + } + + krb5_free_principal(context, princ.principal); + return 0; +} + +int ceo_del_princ(char *user) { + krb5_error_code retval; + krb5_principal princ; + + if ((retval = krb5_parse_name(context, user, &princ))) { + com_err(prog, retval, "while parsing principal name"); + return retval; + } + + retval = kadm5_delete_principal(handle, princ); + if (retval && retval != KADM5_UNK_PRINC) { + com_err(prog, retval, "while deleting principal"); + return retval; + } + + krb5_free_principal(context, princ); + return 0; +} diff --git a/src/kadm.h b/src/kadm.h new file mode 100644 index 0000000..b46c41e --- /dev/null +++ b/src/kadm.h @@ -0,0 +1,5 @@ +void ceo_kadm_init(); +void ceo_kadm_cleanup(); + +int ceo_add_princ(char *, char *); +int ceo_del_princ(char *); diff --git a/src/krb5.c b/src/krb5.c new file mode 100644 index 0000000..a41cafc --- /dev/null +++ b/src/krb5.c @@ -0,0 +1,149 @@ +#include +#include +#include + +#include "krb5.h" +#include "util.h" +#include "config.h" + +extern char *prog; + +krb5_context context; + +static void com_err_hk(const char *whoami, long code, const char *fmt, va_list args) { + char message[4096]; + char *msgp = message; + + msgp += snprintf(msgp, sizeof(message) - 2 - (msgp - message), "%s ", error_message(code)); + if (msgp - message > sizeof(message) - 2) + fatal("error message overflowed"); + + msgp += vsnprintf(msgp, sizeof(message) - 2 - (msgp - message), fmt, args); + if (msgp - message > sizeof(message) - 2) + fatal("error message overflowed"); + + *msgp++ = '\n'; + *msgp++ = '\0'; + + syslog(LOG_ERR, "%s", message); + fprintf(stderr, "%s: %s", whoami, message); +} + +void ceo_krb5_init() { + krb5_error_code retval; + + set_com_err_hook(com_err_hk); + + retval = krb5_init_context(&context); + if (retval) { + com_err(prog, retval, "while initializing krb5"); + exit(1); + } + + retval = krb5_set_default_realm(context, realm); + if (retval) { + com_err(prog, retval, "while setting default realm"); + exit(1); + } +} + +void ceo_krb5_auth(char *principal, char *ktname) { + krb5_error_code retval; + krb5_creds creds; + krb5_principal princ; + krb5_keytab keytab; + krb5_ccache cache; + krb5_get_init_creds_opt options; + + krb5_get_init_creds_opt_init(&options); + memset(&creds, 0, sizeof(creds)); + + if ((retval = krb5_parse_name(context, principal, &princ))) { + com_err(prog, retval, "while resolving user %s", admin_bind_userid); + exit(1); + } + + if ((retval = krb5_cc_default(context, &cache))) { + com_err(prog, retval, "while resolving credentials cache"); + exit(1); + } + + if ((retval = krb5_kt_resolve(context, ktname, &keytab))) { + com_err(prog, retval, "while resolving keytab %s", admin_bind_keytab); + exit(1); + } + + if ((retval = krb5_get_init_creds_keytab(context, &creds, princ, keytab, 0, NULL, &options))) { + com_err(prog, retval, "while getting initial credentials"); + exit(1); + } + + if ((retval = krb5_cc_initialize(context, cache, princ))) { + com_err(prog, retval, "while initializing credentials cache"); + exit(1); + } + + if ((retval = krb5_cc_store_cred(context, cache, &creds))) { + com_err(prog, retval, "while storing credentials"); + exit(1); + } + + krb5_free_cred_contents(context, &creds); + krb5_kt_close(context, keytab); + krb5_free_principal(context, princ); + krb5_cc_close(context, cache); +} + +void ceo_krb5_deauth() { + krb5_error_code retval; + krb5_ccache cache; + + if ((retval = krb5_cc_default(context, &cache))) { + com_err(prog, retval, "while resolving credentials cache"); + exit(1); + } + + if ((retval = krb5_cc_destroy(context, cache))) { + com_err(prog, retval, "while destroying credentials cache"); + exit(1); + } +} + +void ceo_krb5_cleanup() { + krb5_free_context(context); +} + +int ceo_read_password(char *password, unsigned int size, int use_stdin) { + int tries = 0; + unsigned int len; + + do { + if (use_stdin) { + if (fgets(password, size, stdin) == NULL) + fatal("eof while reading password"); + + size = strlen(password); + + if (password[size - 1] == '\n') + password[size - 1] = '\0'; + } else { + len = size; + int retval = krb5_read_password(context, "New password", "Confirm password", password, &len); + if (retval == KRB5_LIBOS_PWDINTR) { + error("interrupted"); + return -1; + } else if (retval == KRB5_LIBOS_BADPWDMATCH) { + fputs("Passwords do not match.\n", stderr); + } else if (!password || !*password) { + fputs("Please enter a password.\n", stderr); + } + } + } while (++tries < 3 && !*password); + + if (!*password) { + error("maximum tries exceeded reading password"); + return -1; + } + + return 0; +} diff --git a/src/krb5.h b/src/krb5.h new file mode 100644 index 0000000..3f1a01c --- /dev/null +++ b/src/krb5.h @@ -0,0 +1,11 @@ +#include + +extern krb5_context context; + +void ceo_krb5_init(); +void ceo_krb5_cleanup(); + +void ceo_krb5_auth(char *, char *); +void ceo_krb5_deauth(); + +int ceo_read_password(char *, unsigned int, int); diff --git a/src/ldap.c b/src/ldap.c new file mode 100644 index 0000000..bd07cff --- /dev/null +++ b/src/ldap.c @@ -0,0 +1,286 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "ldap.h" +#include "krb5.h" +#include "config.h" +#include "util.h" + +extern char *prog; + +LDAP *ld; + +static void ldap_fatal(char *msg) { + int errnum; + char *errstr, *detail; + + ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &errnum); + ldap_get_option(ld, LDAP_OPT_ERROR_STRING, &detail); + + errstr = ldap_err2string(errnum); + + if (detail && *detail) + fatal("%s: %s (%d): %s", msg, errstr, errnum, detail); + else + fatal("%s: %s (%d)", msg, errstr, errnum); +} + +static void ldap_err(char *msg) { + int errnum; + char *errstr, *detail; + + ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &errnum); + ldap_get_option(ld, LDAP_OPT_ERROR_STRING, &detail); + + errstr = ldap_err2string(errnum); + + if (detail && *detail) + error("%s: %s (%d): %s", msg, errstr, errnum, detail); + else + error("%s: %s (%d)", msg, errstr, errnum); +} + +int ceo_add_group(char *cn, char *basedn, int no) { + if (!cn || !basedn) + fatal("addgroup: Invalid argument"); + + LDAPMod *mods[8]; + int i = -1; + int ret = 0; + + mods[++i] = xmalloc(sizeof(LDAPMod)); + mods[i]->mod_op = LDAP_MOD_ADD; + mods[i]->mod_type = "objectClass"; + char *objectClasses[] = { "top", "group", "posixGroup", NULL }; + mods[i]->mod_values = objectClasses; + + mods[++i] = xmalloc(sizeof(LDAPMod)); + mods[i]->mod_op = LDAP_MOD_ADD; + mods[i]->mod_type = "cn"; + char *uids[] = { cn, NULL }; + mods[i]->mod_values = uids; + + mods[++i] = xmalloc(sizeof(LDAPMod)); + mods[i]->mod_op = LDAP_MOD_ADD; + mods[i]->mod_type = "gidNumber"; + char idno[16]; + snprintf(idno, sizeof(idno), "%d", no); + char *gidNumbers[] = { idno, NULL }; + mods[i]->mod_values = gidNumbers; + + mods[++i] = NULL; + + char dn[1024]; + snprintf(dn, sizeof(dn), "cn=%s,%s", cn, basedn); + + if (ldap_add_s(ld, dn, mods) != LDAP_SUCCESS) { + ldap_err("addgroup"); + ret = -1; + } + + i = 0; + while (mods[i]) + free(mods[i++]); + + return ret; +} + +int ceo_add_user(char *uid, char *basedn, char *objclass, char *cn, char *home, char *shell, int no, ...) { + va_list args; + + if (!uid || !basedn || !cn || !home || !shell) + fatal("adduser: Invalid argument"); + + LDAPMod *mods[16]; + int i = -1; + int ret = 0; + + mods[++i] = xmalloc(sizeof(LDAPMod)); + mods[i]->mod_op = LDAP_MOD_ADD; + mods[i]->mod_type = "objectClass"; + char *objectClasses[] = { "top", "account", "posixAccount", "shadowAccount", NULL, NULL }; + if (objclass != NULL) + objectClasses[4] = objclass; + mods[i]->mod_values = objectClasses; + + mods[++i] = xmalloc(sizeof(LDAPMod)); + mods[i]->mod_op = LDAP_MOD_ADD; + mods[i]->mod_type = "uid"; + char *uids[] = { uid, NULL }; + mods[i]->mod_values = uids; + + mods[++i] = xmalloc(sizeof(LDAPMod)); + mods[i]->mod_op = LDAP_MOD_ADD; + mods[i]->mod_type = "cn"; + char *cns[] = { cn, NULL }; + mods[i]->mod_values = cns; + + mods[++i] = xmalloc(sizeof(LDAPMod)); + mods[i]->mod_op = LDAP_MOD_ADD; + mods[i]->mod_type = "uidNumber"; + char idno[16]; + snprintf(idno, sizeof(idno), "%d", no); + char *uidNumbers[] = { idno, NULL }; + mods[i]->mod_values = uidNumbers; + + mods[++i] = xmalloc(sizeof(LDAPMod)); + mods[i]->mod_op = LDAP_MOD_ADD; + mods[i]->mod_type = "gidNumber"; + mods[i]->mod_values = uidNumbers; + + mods[++i] = xmalloc(sizeof(LDAPMod)); + mods[i]->mod_op = LDAP_MOD_ADD; + mods[i]->mod_type = "homeDirectory"; + char *homeDirectory[] = { home, NULL }; + mods[i]->mod_values = homeDirectory; + + va_start(args, no); + char *attr; + while ((attr = va_arg(args, char *))) { + char *val = va_arg(args, char *); + + if (!val || !*val) + continue; + + if (i == sizeof(mods) / sizeof(*mods) - 2) { + error("too many attributes"); + return -1; + } + + mods[++i] = xmalloc(sizeof(LDAPMod)); + mods[i]->mod_op = LDAP_MOD_ADD; + mods[i]->mod_type = attr; + char *vals[] = { val, NULL }; + mods[i]->mod_values = vals; + } + + mods[++i] = NULL; + + char dn[1024]; + snprintf(dn, sizeof(dn), "uid=%s,%s", uid, basedn); + + if (ldap_add_s(ld, dn, mods) != LDAP_SUCCESS) { + ldap_err("adduser"); + ret = -1; + } + + i = 0; + while (mods[i]) + free(mods[i++]); + + return ret; +} + +int ceo_new_uid(int min, int max) { + char filter[64]; + char *attrs[] = { LDAP_NO_ATTRS, NULL }; + LDAPMessage *res; + int i; + + for (i = min; i <= max; i++) { + // id taken due to passwd + if (getpwuid(i) != NULL) + continue; + + // id taken due to group + if (getgrgid(i) != NULL) + continue; + + snprintf(filter, sizeof(filter), "(|(uidNumber=%d)(gidNumber=%d))", i, i); + if (ldap_search_s(ld, users_base, LDAP_SCOPE_SUBTREE, filter, attrs, 1, &res) != LDAP_SUCCESS) { + ldap_err("firstuid"); + return -1; + } + + int count = ldap_count_entries(ld, res); + ldap_msgfree(res); + + // id taken due to LDAP + if (count) + continue; + + return i; + } + + return -1; +} + +int ceo_add_club(char *uid, char *cn) { + int id = ceo_new_uid(club_min_id, club_max_id); + + if (ceo_add_user(uid, users_base, "club", cn, club_home, club_shell, id, NULL)) + return -1; + + if (ceo_add_group(uid, groups_base, id)) + return -1; + + return 0; +} + +int ceo_user_exists(char *uid) { + char *attrs[] = { LDAP_NO_ATTRS, NULL }; + LDAPMessage *msg = NULL; + char filter[128]; + int count; + + if (!uid) + fatal("null uid"); + + snprintf(filter, sizeof(filter), "uid=%s", uid); + + if (ldap_search_s(ld, users_base, LDAP_SCOPE_SUBTREE, filter, attrs, 0, &msg) != LDAP_SUCCESS) { + ldap_err("user_exists"); + return -1; + } + + count = ldap_count_entries(ld, msg); + ldap_msgfree(msg); + + return count > 0; +} + +static int ldap_sasl_interact(LDAP *ld, unsigned flags, void *defaults, void *in) { + sasl_interact_t *interact = in; + + while (interact->id != SASL_CB_LIST_END) { + switch (interact->id) { + + // GSSAPI doesn't require any callbacks + + default: + interact->result = ""; + interact->len = 0; + } + + interact++; + } + + return LDAP_SUCCESS; +} + +void ceo_ldap_init() { + int proto = LDAP_DEFAULT_PROTOCOL; + + if (ldap_initialize(&ld, server_url) != LDAP_SUCCESS) + ldap_fatal("ldap_initialize"); + + if (ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &proto) != LDAP_OPT_SUCCESS) + ldap_fatal("ldap_set_option"); + + ceo_krb5_auth(admin_bind_userid, admin_bind_keytab); + + if (ldap_sasl_interactive_bind_s(ld, NULL, sasl_mech, NULL, NULL, + LDAP_SASL_QUIET, &ldap_sasl_interact, NULL) != LDAP_SUCCESS) + ldap_fatal("Bind failed"); + + ceo_krb5_deauth(); +} + +void ceo_ldap_cleanup() { + ldap_unbind(ld); +} diff --git a/src/ldap.h b/src/ldap.h new file mode 100644 index 0000000..46b192c --- /dev/null +++ b/src/ldap.h @@ -0,0 +1,10 @@ +#define LDAP_DEFAULT_PROTOCOL LDAP_VERSION3 + +int ceo_add_user(char *, char *, char *, char *, char *, char *, int, ...); +int ceo_add_group(char *, char *, int); +int ceo_new_uid(int, int); + +void ceo_ldap_init(); +void ceo_ldap_cleanup(); + +int ceo_user_exists(char *); diff --git a/src/parser.c b/src/parser.c new file mode 100644 index 0000000..a29262d --- /dev/null +++ b/src/parser.c @@ -0,0 +1,214 @@ +#include +#include +#include +#include +#include +#include + +#include "parser.h" +#include "util.h" +#include "config.h" + +#define VAR_MAX 256 + +void config_var(const char *, const char *); + +struct config_file { + FILE *p; + char *name; + int line; + struct config_file *parent; + int comment; +}; + +static void parse_config_file(char *, struct config_file *); +static void parse_error(struct config_file *file, char *msg) { + fatal("parse error on line %d of %s: %s", file->line, file->name, msg); +} + +static int parse_char(struct config_file *file) { + int c = getc(file->p); + if (c == '\n') + (file->line)++; + return c; +} + +static void unparse_char(struct config_file *file, int c) { + if (c == EOF) + return; + ungetc(c, file->p); + if (c == '\n') + (file->line)--; +} + +static void parse_name(struct config_file *file, char *name, size_t maxlen) { + int len = 0; + int c; + + for (;;) { + c = parse_char(file); + + if (c == EOF || c == '\n') { + unparse_char(file, c); + break; + } + + if (!isalpha(c) && c != '_' && c != '-') { + unparse_char(file, c); + break; + } + + if (len == maxlen - 1) + parse_error(file, "max name length exceeded"); + + name[len++] = c; + } + + if (len == 0) + parse_error(file, "expected name"); + + name[len++] = '\0'; +} + +static void parse_value(struct config_file *file, char *value, size_t maxlen) { + int len = 0; + int quote = 0; + int comment = 0; + int space = 0; + int c; + + for (;;) { + c = parse_char(file); + + if (c == EOF || c == '\n') + break; + + if (c == '#') + comment = 1; + + if ((isspace(c) && !quote) || comment) { + space = 1; + continue; + } + + if (c == '"') { + quote = ! quote; + continue; + } + + if (len == maxlen - space - 1) + parse_error(file, "max value length exceeded"); + + if (space && len) { + value[len++] = ' '; + } + + space = 0; + value[len++] = c; + } + + if (quote) + parse_error(file, "unbalanced quotes"); + + value[len++] = '\0'; +} + +static void parse_include(struct config_file *file) { + char path[PATH_MAX]; + struct config_file *parent = file->parent; + + parse_value(file, path, sizeof(path)); + + while (parent != NULL) { + if (!strcmp(file->name, parent->name)) + return; + parent = parent->parent; + } + + parse_config_file(path, file); +} + +static void parse_config(struct config_file *file) { + int c; + int comment = 0; + + char var[VAR_MAX]; + char value[VAR_MAX]; + + for (;;) { + c = parse_char(file); + + if (c == '\n') { + comment = 0; + continue; + } + + if (c == EOF) + return; + + if (isspace(c) | comment) + continue; + + if (c == '#') { + comment = 1; + continue; + } + + unparse_char(file, c); + parse_name(file, var, sizeof(var)); + + if (!strcmp(var, "include")) { + parse_include(file); + continue; + } + + for (;;) { + c = parse_char(file); + if (c == EOF || c == '\n') + parse_error(file, "expected '=' before line end"); + if (c == '=') + break; + if (!isspace(c)) + parse_error(file, "expected '='"); + } + + parse_value(file, value, sizeof(value)); + config_var(var, value); + } +} + +static void parse_config_file(char *name, struct config_file *parent) { + struct config_file file; + + file.p = fopen(name, "r"); + file.name = name; + file.line = 1; + file.parent = parent; + + if (!file.p) { + if (parent) + parse_error(parent, strerror(errno)); + else + fatal("failed to open configuration file '%s': %s", name, strerror(errno)); + } + + parse_config(&file); + + fclose(file.p); +} + +long config_long(char *var, char *val) { + char *endptr; + long longval; + + longval = strtol(val, &endptr, 0); + + if (*val == '\0' || *endptr != '\0') + fatal("expected integer value for %s", var); + + return longval; +} + +void config_parse(char *filename) { + parse_config_file(filename, NULL); +} diff --git a/src/parser.h b/src/parser.h new file mode 100644 index 0000000..27f8bb9 --- /dev/null +++ b/src/parser.h @@ -0,0 +1,2 @@ +void config_parse(char *); +long config_long(char *var, char *val); diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..917b864 --- /dev/null +++ b/src/util.c @@ -0,0 +1,136 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" + +static char message[4096]; + +static void errmsg(int prio, const char *prefix, const char *fmt, va_list args) { + char *msgp = message; + + msgp += snprintf(msgp, sizeof(message) - 2 - (msgp - message), "%s: ", prefix); + if (msgp - message > sizeof(message) - 2) + fatal("error message overflowed"); + + msgp += vsnprintf(msgp, sizeof(message) - 2 - (msgp - message), fmt, args); + if (msgp - message > sizeof(message) - 2) + fatal("error message overflowed"); + + *msgp++ = '\n'; + *msgp++ = '\0'; + + syslog(prio, "%s", message); + fputs(message, stderr); +} + +static void errmsgpe(int prio, const char *prefix, const char *fmt, va_list args) { + char *msgp = message; + + msgp += snprintf(msgp, sizeof(message) - 2 - (msgp - message), "%s: ", prefix); + if (msgp - message > sizeof(message) - 2) + fatal("error message overflowed"); + + msgp += vsnprintf(msgp, sizeof(message) - 2 - (msgp - message), fmt, args); + if (msgp - message > sizeof(message) - 2) + fatal("error message overflowed"); + + msgp += snprintf(msgp, sizeof(message) - 2 - (msgp - message), ": %s", strerror(errno)); + if (msgp - message > sizeof(message) - 2) + fatal("error message overflowed"); + + *msgp++ = '\n'; + *msgp++ = '\0'; + + syslog(prio, "%s", message); + fputs(message, stderr); +} + +NORETURN static void die(int prio, const char *prefix, const char *msg, va_list args) { + errmsg(prio, prefix, msg, args); + exit(1); +} + +NORETURN static void diepe(int prio, const char *prefix, const char *msg, va_list args) { + errmsgpe(prio, prefix, msg, args); + exit(1); +} + +NORETURN void fatal(const char *msg, ...) { + va_list args; + va_start(args, msg); + die(LOG_CRIT, "fatal", msg, args); + va_end(args); +} + +void error(const char *msg, ...) { + va_list args; + va_start(args, msg); + errmsg(LOG_ERR, "error", msg, args); + va_end(args); +} + +void warn(const char *msg, ...) { + va_list args; + va_start(args, msg); + errmsg(LOG_WARNING, "warning", msg, args); + va_end(args); +} + +void logmsg(const char *msg, ...) { + va_list args; + va_start(args, msg); + vsyslog(LOG_ERR, msg, args); + va_end(args); +} + +NORETURN void deny(const char *msg, ...) { + va_list args; + va_start(args, msg); + die(LOG_ERR, "denied", msg, args); + va_end(args); +} + +NORETURN void badconf(const char *msg, ...) { + va_list args; + va_start(args, msg); + die(LOG_CRIT, "configuration error", msg, args); + va_end(args); +} + +NORETURN void fatalpe(const char *msg, ...) { + va_list args; + va_start(args, msg); + diepe(LOG_CRIT, "fatal", msg, args); + va_end(args); +} + +void errorpe(const char *msg, ...) { + va_list args; + va_start(args, msg); + errmsgpe(LOG_ERR, "error", msg, args); + va_end(args); +} + +void warnpe(const char *msg, ...) { + va_list args; + va_start(args, msg); + errmsgpe(LOG_WARNING, "warning", msg, args); + va_end(args); +} + +int spawnv(const char *path, char *argv[]) { + int pid, status; + pid = fork(); + if (pid == -1) + fatalpe("fork"); + else if (pid) + waitpid(pid, &status, 0); + else + exit(execv(path, argv)); + return status; +} diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..f8ad49c --- /dev/null +++ b/src/util.h @@ -0,0 +1,44 @@ +#ifndef CEO_UTIL_H +#define CEO_UTIL_H + +#include +#include +#include + +#ifdef __GNUC__ +#define NORETURN __attribute__((__noreturn__)) +#else +#define NORETURN +#endif + +int spawnv(const char *, char *[]); + +NORETURN void fatal(const char *, ...); +NORETURN void fatalpe(const char *, ...); +NORETURN void badconf(const char *, ...); +NORETURN void deny(const char *, ...); +void error(const char *, ...); +void warn(const char *, ...); +void logmsg(const char *, ...); +void errorpe(const char *, ...); +void warnpe(const char *, ...); + +static inline void *xmalloc(size_t size) { + void *alloc = malloc(size); + + if (alloc == NULL) + fatal("out of memory"); + + return alloc; +} + +static inline char *xstrdup(const char *s) { + char *dup = strdup(s); + + if (dup == NULL) + fatal("out of memory"); + + return dup; +} + +#endif