]> oss.titaniummirror.com Git - repo_shell.git/blobdiff - repo_shell.c
gitcreate: add option to set description
[repo_shell.git] / repo_shell.c
index 35edbe7c790f9cf1ff864a2a7250699464f22c47..07e5cf7e4b24e0d5a4275fc1b542def979af7052 100644 (file)
@@ -1,3 +1,4 @@
+#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 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
-
-#define alloc_nr(x) (((x)+16)*3/2)
-
-/*
- * Realloc the buffer pointed at by variable 'x' so that it can hold
- * at least 'nr' entries; the number of entries currently allocated
- * is 'alloc', using the standard growing factor alloc_nr() macro.
- *
- * DO NOT USE any expression with side-effect for 'x', 'nr', or 'alloc'.
- */
-#define ALLOC_GROW(x, nr, alloc) \
-       do { \
-               if ((nr) > alloc) { \
-                       if (alloc_nr(alloc) < (nr)) \
-                               alloc = (nr); \
-                       else \
-                               alloc = alloc_nr(alloc); \
-                       x = xrealloc((x), alloc * sizeof(*(x))); \
-               } \
-       } while (0)
-
-static inline 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;
-}
-
-void *xrealloc(void *ptr, size_t size)
-{
-       void *ret;
-
-       ret = realloc(ptr, size);
-       if (!ret && !size)
-               ret = realloc(ptr, 1);
-       if (!ret)
-               die("Out of memory, realloc failed");
-       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;
+  char* narg = NULL;
 
-       if (arg && *arg == '\'') {
-               char* end = arg + strlen(arg) - 1;
+  if (arg && *arg == '\'') {
+    char* end = arg + strlen(arg) - 1;
 
-               if (end != arg && *end == '\'') {
-                       narg = arg + 1;
-                       *end = '\0';
-               }
-       }
-       return narg;
+    if (end != arg && *end == '\'') {
+      narg = arg + 1;
+      *end = '\0';
+    }
+  }
+  return narg;
 }
 
-static char *add_prefix(char *prefix, char* arg)
+static char *add_prefix(const char *prefix, const char* arg)
 {
-       int size;
-
-       if (arg && prefix && strlen(prefix)) {
-               char *n = xmalloc(sizeof(char *) *
-                       (strlen(prefix) + strlen(arg) + 2));
-               strcpy(n, prefix);
-               strcat(n, "/");
-               strcat(n, arg);
-               arg = n;
-       }
-       return arg;
+  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 check_ssh_interactive(uid_t uid)
+/* Return true if the user's permissions >= those required */
+static bool git_check_access(const char *cmd, const char *repo,
+    const char *user)
 {
-       /* 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 */
-}
-
-static int git_check_access(const char *cmd, const char *arg, const char *user)
-{
-       /* TODO: Read some configuration file which maps users and access
-        * to a boolean true/false value.
-        *
-        * The git command can support read and write.
-        * git-receive-pack is ok for readers and writers
-        * git-upload-pack is ok only for writers
-        * git-upload-archive is ok only for writers
-        */
-       return 1; /* assume OK for now */
+  /* 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);
-}
-
-#define SPLIT_CMDLINE_BAD_ENDING 1
-#define SPLIT_CMDLINE_UNCLOSED_QUOTE 2
-static const char *split_cmdline_errors[] = {
-       "cmdline ends with \\",
-       "unclosed quote"
-};
-
-int split_cmdline(char *cmdline, const char ***argv)
-{
-       int src, dst, count = 0, size = 16;
-       char quoted = 0;
-
-       *argv = xmalloc(sizeof(char *) * size);
-
-       /* split alias_string */
-       (*argv)[count++] = cmdline;
-       for (src = dst = 0; cmdline[src];) {
-               char c = cmdline[src];
-               if (!quoted && isspace(c)) {
-                       cmdline[dst++] = 0;
-                       while (cmdline[++src]
-                                       && isspace(cmdline[src]))
-                               ; /* skip */
-                       ALLOC_GROW(*argv, count+1, size);
-                       (*argv)[count++] = cmdline + dst;
-               } else if (!quoted && (c == '\'' || c == '"')) {
-                       quoted = c;
-                       src++;
-               } else if (c == quoted) {
-                       quoted = 0;
-                       src++;
-               } else {
-                       if (c == '\\' && quoted != '\'') {
-                               src++;
-                               c = cmdline[src];
-                               if (!c) {
-                                       free(*argv);
-                                       *argv = NULL;
-                                       return -SPLIT_CMDLINE_BAD_ENDING;
-                               }
-                       }
-                       cmdline[dst++] = c;
-                       src++;
-               }
-       }
-
-       cmdline[dst] = 0;
-
-       if (quoted) {
-               free(*argv);
-               *argv = NULL;
-               return -SPLIT_CMDLINE_UNCLOSED_QUOTE;
-       }
-
-       ALLOC_GROW(*argv, count+1, size);
-       (*argv)[count] = NULL;
-
-       return count;
-}
-
-const char *split_cmdline_strerror(int split_cmdline_errno) {
-       return split_cmdline_errors[-split_cmdline_errno-1];
+  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("could not determine user's home directory; 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 handler(void* user, const char* section, const char* name,
-                   const char* value)
+static int ini_handler(void* user, const char* section, const char* name,
+    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 < 3)
-               die("invalid arguments");
-
-#ifdef USE_DEFAULTS
-       ini_parse("repo_shell.cfg", handler, &cfg);
-#else
-       if (ini_parse(CFG_FILE, 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");
-
-       cd_to_homedir();
-       count = split_cmdline(prog, &user_argv);
-       if (count >= 0) {
-               execvp(user_argv[0], (char *const *) user_argv);
-               free(user_argv);
-               die("unrecognized command '%s'", argv[2]);
-       } else {
-               free(prog);
-               die("invalid command format '%s': %s", argv[2],
-                   split_cmdline_strerror(count));
-       }
+  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);
 }