+#include <stdbool.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <pwd.h>
#include <string.h>
#include "ini.h"
+#include "utility.h"
#include "version.h"
+#include "git_acl.h"
+#include "stringutils.h"
-#define CFG_FILE "/etc/repo_shell.cfg"
-#define GIT_ACL_FILE "git_acl.cfg"
+#define CFG_FILE "/etc/repo_shell.conf"
#define SHELL "/bin/bash"
+#define GIT_ACL_FILE ".gitacls"
+
+enum { REPO_UMASK = 027 };
typedef struct {
- char *svn_root;
- char *git_root;
- char *owner;
+ char *user;
+ char *svn_root;
+ char *git_root;
+ char *owner;
+ char *git_acl_file;
+ bool allow_interactive;
} cfg_t;
-#undef USE_DEFAULTS
-#ifdef USE_DEFAULTS /* perhaps we want defaults? Not sure */
-static cfg_t cfg {
- svn_root: "/var/lib/svn/repositories",
- git_root: "/var/lib/git",
- owner: "repo"
-};
-#else
static cfg_t cfg;
-#endif
-
-static void die(const char *fmt, ...)
-{
- va_list ap;
-
- va_start(ap, fmt);
- fprintf(stderr, "error: ");
- vfprintf(stderr, fmt, ap);
- fprintf(stderr, "\n" );
- va_end(ap);
- exit(1);
-}
-
-char *xstrdup(const char *str)
-{
- char *ret = strdup(str);
- if (!ret)
- die("out of memory");
- return ret;
-}
-
-void *xmalloc(size_t size)
-{
- void *ret;
-
- ret = malloc(size);
- if (!ret && !size)
- ret = malloc(1);
- if (!ret)
- die("out of memory");
- return ret;
-}
-static uid_t user_uid(char *user)
+/* This is the function for which setuid root is needed for repo_shell */
+static void change_user(char *user)
{
- struct passwd *pw = getpwnam(user);
+ struct passwd *pw = getpwnam(user);
- if (!pw)
- die("invalid user %s", user);
- return pw->pw_uid;
+ if (!pw)
+ die("invalid user %s", user);
+ setgid(pw->pw_gid);
+ setuid(pw->pw_uid);
}
-static void change_user(char *user)
+/* Set the user and group permissions back to the requesting user */
+static void reset_user()
{
- /* This is the function for which setuid is required, as root */
- setuid(user_uid(user));
+ setgid(getgid());
+ setuid(getuid());
}
static char *dequote(char *arg)
{
- char* narg = NULL;
-
- if (arg && *arg == '\'') {
- char* end = arg + strlen(arg) - 1;
-
- if (end != arg && *end == '\'') {
- narg = arg + 1;
- *end = '\0';
- }
- }
- return narg;
-}
+ char* narg = NULL;
-static char *add_prefix(char *prefix, char* arg)
-{
- char *narg = arg;
- int i;
-
- if (arg && prefix && (i = strlen(prefix))) {
- narg = xmalloc(sizeof(char *) * (i + strlen(arg) + 2));
- strcpy(narg, prefix);
- strcpy(narg + i++, "/");
- strcpy(narg + i, arg);
- }
- return narg;
-}
+ if (arg && *arg == '\'') {
+ char* end = arg + strlen(arg) - 1;
-static int check_ssh_interactive(uid_t uid)
-{
- /* TODO: Check the config file for the user owning uid to see if that
- * user should be able to execute any commands other than those required
- * to support repository access. Return a boolean true/false.
- */
- return 1; /* for now */
+ if (end != arg && *end == '\'') {
+ narg = arg + 1;
+ *end = '\0';
+ }
+ }
+ return narg;
}
-static int git_acl(const char *user, const char *repo)
+static char *add_prefix(const char *prefix, const char* arg)
{
- /* TODO: Read GIT_ACL_FILE from cfg.owner's home directory. Look for
- * the access level afforded user for repo. A return of 0 means no
- * access, a return of 1 means read only, and a return of 2 means
- * read/write.
- */
-#if 0
- struct passwd *pw;
- char *file;
- int len = strlen(cfg.owner) + strlen(GIT_ACL_FILE) + 8;
-
- pw = getpwnam(cfg.owner);
- if (!pw)
- die("owner %s has no passwd entry?", cfg.owner);
- len = strlen(pw->pw_dir) + strlen(GIT_ACL_FILE) + 2;
- file = xmalloc(sizeof(char) * len);
- sprintf(file, "%s/%s", pw->pw_dir, GIT_ACL_FILE);
- fprintf(stderr, "[someday check %s for git ACLs]\n", file);
- free(file);
-#endif
- return 2; /* assume read/write for now */
+ char *narg;
+ int i;
+
+ if (arg && prefix && (i = strlen(prefix))) {
+ narg = xmalloc(sizeof(char *) * (i + strlen(arg) + 2));
+ strcpy(narg, prefix);
+ strcpy(narg + i++, "/");
+ strcpy(narg + i, arg);
+ }
+ return narg;
}
-static int git_check_access(const char *cmd, const char *repo, const char *user)
+/* Return true if the user's permissions >= those required */
+static bool git_check_access(const char *cmd, const char *repo,
+ const char *user)
{
- /* What access is required per the incoming command?
- * 0=none, 1=read-only, 2=read-write
- */
- int rw = (!strcmp(cmd, "git-upload-pack") ||
- !strcmp(cmd, "git-upload-archive")) ? 2 : 1;
-
- /* Return true (1) if the user permissions >= those required */
- return (git_acl(user, repo) >= rw) ? 1 : 0;
+ /* What access is required per the incoming command? Uploading commands
+ * require PERMS_READ_WRITE, while all other commands are assumed to need only
+ * PERMS_READ.
+ */
+ perms_t need = !strncmp(cmd, "git-upload", 10) ? PERMS_READ :
+ PERMS_READ_WRITE;
+ perms_t have = git_acl(user, repo, cfg.git_acl_file);
+ return have >= need;
}
static int do_git_cmd(const char *cmd, char *arg, char *user)
{
- const char *nargv[4];
- char* narg;
- int ret;
-
- if (!(arg = dequote(arg)))
- die("bad argument");
- if (strncmp(cmd, "git-", 4))
- die("bad command");
-
- change_user(cfg.owner);
- if (!git_check_access(cmd, arg, user))
- die("permission denied");
-
- nargv[0] = cmd;
- nargv[1] = add_prefix(cfg.git_root, arg);
- nargv[2] = NULL;
-
- ret = execvp(nargv[0], (char *const *) nargv);
- /* Code unreached if execv successful */
- free(narg);
- return ret;
+ const char *nargv[4];
+ char* narg;
+ int ret;
+
+ if (!(arg = dequote(arg)))
+ die("bad argument");
+ if (strncmp(cmd, "git-", 4))
+ die("bad command");
+
+ change_user(cfg.owner);
+ umask(REPO_UMASK);
+ if (!git_check_access(cmd, arg, user))
+ die("insufficient ACL permissions");
+
+ nargv[0] = cmd;
+ nargv[1] = add_prefix(cfg.git_root, arg);
+ nargv[2] = NULL;
+
+ ret = execvp(nargv[0], (char *const *) nargv);
+ /* Code unreached if execv successful */
+ free((char*)nargv[1]);
+ free(narg);
+ return ret;
}
static int do_svnserve_cmd(const char *cmd, char *arg, char *user)
{
- const char *svnserve_argv[7] = {
- cmd, "-t", "--root", cfg.svn_root, "--tunnel-user", user, NULL
- };
- int ret;
-
- change_user(cfg.owner);
- return execvp(svnserve_argv[0], (char *const *) svnserve_argv);
+ const char *svnserve_argv[7] = {
+ cmd, "-t", "--root", cfg.svn_root, "--tunnel-user", user, NULL
+ };
+ int ret;
+
+ change_user(cfg.owner);
+ umask(REPO_UMASK);
+ return execvp(svnserve_argv[0], (char *const *) svnserve_argv);
}
static void cd_to_homedir(void)
{
- const char *home = getenv("HOME");
- if (!home)
- die("user variable HOME is unset");
- if (chdir(home) == -1)
- die("could not chdir to user's home directory");
+ const char *home = getenv("HOME");
+ if (!home)
+ die("user variable HOME is unset");
+ if (chdir(home) == -1)
+ die("could not chdir to user's home directory");
}
static struct commands {
- const char *name;
- int (*exec)(const char *cmd, char *arg, char *user);
+ const char *name;
+ int (*exec)(const char *cmd, char *arg, char *user);
} cmd_list[] = {
- { "git-receive-pack", do_git_cmd },
- { "git-upload-pack", do_git_cmd },
- { "git-upload-archive", do_git_cmd },
- { "svnserve", do_svnserve_cmd },
- { NULL },
+ { "git-receive-pack", do_git_cmd },
+ { "git-upload-pack", do_git_cmd },
+ { "git-upload-archive", do_git_cmd },
+ { "svnserve", do_svnserve_cmd },
+ { NULL },
};
static int ini_handler(void* user, const char* section, const char* name,
- const char* value)
+ const char* value)
{
- cfg_t* pconfig = (cfg_t*)user;
-
- #define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0
- if (MATCH("core", "svn_root"))
- pconfig->svn_root = xstrdup(value);
- else if (MATCH("core", "git_root"))
- pconfig->git_root = xstrdup(value);
- else if (MATCH("core", "owner"))
- pconfig->owner = xstrdup(value);
- else
- return 0; /* unknown section/name, error */
- return 1;
+ cfg_t* pconfig = (cfg_t*)user;
+
+ if (!strcmp(name, "svn_root"))
+ pconfig->svn_root = xstrdup(value);
+ else if (!strcmp(name, "git_root")) {
+ pconfig->git_root = xstrdup(value);
+ pconfig->git_acl_file = add_prefix(value, GIT_ACL_FILE);
+ } else if (!strcmp(name, "owner"))
+ pconfig->owner = xstrdup(value);
+ else if (!strcmp(name, "allow_interactive"))
+ pconfig->allow_interactive = str_has_word(value, pconfig->user);
+ else
+ return 0; /* unknown section/name, error */
+ return 1;
}
int main(int argc, char **argv)
{
- char *prog;
- const char **user_argv;
- struct commands *cmd;
- int devnull_fd;
- int count;
-
- /*
- * Always open file descriptors 0/1/2 to avoid clobbering files
- * in die(). It also avoids not messing up when the pipes are
- * dup'ed onto stdin/stdout/stderr in the child processes we spawn.
- */
- devnull_fd = open("/dev/null", O_RDWR);
- while (devnull_fd >= 0 && devnull_fd <= 2)
- devnull_fd = dup(devnull_fd);
- if (devnull_fd == -1)
- die("opening /dev/null failed");
- close (devnull_fd);
-
- if (argc == 2 && (!strcmp(argv[1], "-v") ||
- !strcmp(argv[1], "--version"))) {
- fprintf(stderr, "%s\n", version);
- return 0;
- }
-
- if (argc == 1 && check_ssh_interactive(getuid())) {
- setuid(getuid());
- argv[0] = SHELL;
- execvp(argv[0], (char *const *) argv);
- }
-
-#ifdef USE_DEFAULTS
- ini_parse("repo_shell.cfg", ini_handler, &cfg);
-#else
- if (ini_parse(CFG_FILE, ini_handler, &cfg) < 0)
- die("cannot read config file %s", CFG_FILE);
-#endif
-
- prog = xstrdup(argv[2]);
- if (!strncmp(prog, "git", 3) && isspace(prog[3]))
- /* Accept "git foo" as if the caller said "git-foo". */
- prog[3] = '-';
-
- for (cmd = cmd_list ; cmd->name ; cmd++) {
- int len = strlen(cmd->name);
- char *arg;
- struct passwd *pw;
- if (strncmp(cmd->name, prog, len))
- continue;
- arg = NULL;
- switch (prog[len]) {
- case '\0':
- arg = NULL;
- break;
- case ' ':
- arg = prog + len + 1;
- break;
- default:
- continue;
- }
-
- pw = getpwuid(getuid());
- exit(cmd->exec(cmd->name, arg, pw->pw_name));
- }
-
- if (!check_ssh_interactive(getuid()))
- die("only repository access is allowed");
-
- setuid(getuid());
- cd_to_homedir();
- argv[0] = SHELL;
- execvp(argv[0], (char *const *) argv);
+ char *prog;
+ const char **user_argv;
+ struct commands *cmd;
+ int devnull_fd;
+ int count;
+ struct passwd *pw;
+
+ /*
+ * Always open file descriptors 0/1/2 to avoid clobbering files
+ * in die(). It also avoids not messing up when the pipes are
+ * dup'ed onto stdin/stdout/stderr in the child processes we spawn.
+ */
+ devnull_fd = open("/dev/null", O_RDWR);
+ while (devnull_fd >= 0 && devnull_fd <= 2)
+ devnull_fd = dup(devnull_fd);
+ if (devnull_fd == -1)
+ die("opening /dev/null failed");
+ close (devnull_fd);
+
+ if (argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help"))) {
+ fprintf(stderr, "%s is a replacement login shell.\n"
+ " May be ran from the command line with one of these options:\n"
+ " -h|--help this text\n"
+ " -v|--version program version string\n"
+ " -t|--test <user> <repo> test access\n"
+ " -d|--detail <user> <repo> test access, outputting more detail\n"
+ , argv[0]);
+ return 0;
+ }
+
+ if (argc == 2 && (!strcmp(argv[1], "-v") || !strcmp(argv[1], "--version"))) {
+ fprintf(stderr, "%s\n", version);
+ return 0;
+ }
+
+ pw = getpwuid(getuid());
+ cfg.user = xstrdup(pw->pw_name);
+ if (ini_parse(CFG_FILE, ini_handler, &cfg) < 0)
+ die("cannot read config file %s", CFG_FILE);
+
+ if (argc == 1) {
+ if (!cfg.allow_interactive) {
+ fprintf(stderr, "\n");
+ die("only repository access is allowed");
+ }
+ reset_user();
+ argv[0] = SHELL;
+ execvp(argv[0], (char *const *) argv);
+ return 1;
+ }
+
+ if ((!strcmp(argv[1], "-d") || !strcmp(argv[1], "--detail"))) {
+ perms_t p;
+
+ if (argc !=4)
+ die("usage: %s -d|--detail <user> <repo>", argv[0]);
+ p = git_acl(argv[2], argv[3], cfg.git_acl_file);
+ fprintf(stderr,
+ "user '%s' repo '%s' perms '%s'\n via userid '%s' repoid '%s'\n",
+ argv[2], argv[3], git_acl_perms_as_str(p), git_acl_last_userid(),
+ git_acl_last_repoid());
+ return 0;
+ }
+
+ if ((!strcmp(argv[1], "-t") || !strcmp(argv[1], "--test"))) {
+ perms_t p;
+
+ if (argc !=4)
+ die("usage: %s -t|--test <user> <repo>", argv[0]);
+ p = git_acl(argv[2], argv[3], cfg.git_acl_file);
+ printf("%s\n", git_acl_perms_as_str(p));
+ return 0;
+ }
+
+ if (argc == 3) {
+ /* argv[0] = repo_shell, argv[1] = -c, argv[2] = cmd
+ * cmd = "svnserve -t" or "git-xxx '/path/to/repo.git'"
+ */
+ prog = xstrdup(argv[2]);
+ if (!strncmp(prog, "git", 3) && isspace(prog[3]))
+ /* Accept "git foo" as if the caller said "git-foo". */
+ prog[3] = '-';
+
+ for (cmd = cmd_list ; cmd->name ; cmd++) {
+ int len = strlen(cmd->name);
+ char *arg;
+ if (strncmp(cmd->name, prog, len))
+ continue;
+ arg = NULL;
+ switch (prog[len]) {
+ case '\0':
+ arg = NULL;
+ break;
+ case ' ':
+ arg = prog + len + 1;
+ break;
+ default:
+ continue;
+ }
+
+ exit(cmd->exec(cmd->name, arg, cfg.user));
+ }
+ }
+
+ if (!cfg.allow_interactive)
+ die("only repository access is allowed");
+ reset_user();
+ cd_to_homedir();
+ argv[0] = SHELL;
+ execvp(argv[0], (char *const *) argv);
}