summaryrefslogtreecommitdiff
path: root/src/main.c
blob: 9fb7f31b56b08b152fd6f24dcaf58ece0af0bbdf (plain)
    1 /**
    2  * Gitaccess implements basic access controls for git servers.
    3  * Copyright (C) 2020 Aaron Ball <nullspoon@oper.io>
    4  * 
    5  * This program is free software: you can redistribute it and/or modify
    6  * it under the terms of the GNU General Public License as published by
    7  * the Free Software Foundation, either version 3 of the License, or
    8  * (at your option) any later version.
    9  * 
   10  * This program is distributed in the hope that it will be useful,
   11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
   12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   13  * GNU General Public License for more details.
   14  * 
   15  * You should have received a copy of the GNU General Public License
   16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
   17  */
   18 
   19 /**
   20  * Description
   21  * -----------
   22  * 
   23  * This script provides basic access controls to git repos. For this script to
   24  * work, it requires that each repository has a 'users' file. This script reads
   25  * that file and determines if the user associated with the logged in ssh key
   26  * has access to that repo.
   27  * 
   28  * This script also provides support for interactive git shell, interactive
   29  * shell rejection via the no-interactive-shell script, and any other scripts
   30  * that are placed inside the ~/git-shell-commands directory.
   31  * 
   32  * To use this script for a specified ssh key, call it using the command
   33  * directive in the ~/.ssh/authorized keys file using the following syntax
   34  * 
   35  *     # Key for user <username>
   36  *     command="gitaccess <username>" ecdsa-sha2-nistp521 AAAAE2v....
   37  */
   38 #include <stdio.h>
   39 #include <stdlib.h>
   40 #include <string.h>
   41 #include <unistd.h>
   42 #include <time.h>
   43 
   44 extern char **environ;
   45 
   46 /**
   47  * logmsg:
   48  * Writes specified message to log file at ${HOME}/git.log
   49  *
   50  * @msg  Message to write to log file
   51  */
   52 void logmsg(char* msg) {
   53   char log[512];
   54   // Get the current timestamp
   55   char dstr[100];
   56   time_t now = time(NULL);
   57   struct tm* t = localtime(&now);
   58 
   59   // Generate date string
   60   strftime(dstr, sizeof(dstr)-1, "%Y-%m-%d  %H:%M:%S", t);
   61 
   62   sprintf(log, "%s/%s", getenv("HOME"), "git.log");
   63 
   64   FILE* fd = fopen(log, "a");
   65   fprintf(fd, "%s %s\n", dstr, msg);
   66   fclose(fd);
   67 }
   68 
   69 /**
   70  * unwrap:
   71  * Grabs the text between a delimiter (usually a quote). Will traverse the
   72  * provided string until the first occurence of the delimiter is found, and
   73  * will continue until the closing delimiter is found (or end of line).
   74  *
   75  * @wrapchar  Delimiter character wrapping the string
   76  * @line      Line to search for the wrapped string
   77  * @buf       Output buffer that will contain the wrapped string
   78  *
   79  * @return    Pointer to the buffer
   80  */
   81 char* unwrap(char wrapchar, char* line, char* buf) {
   82   char *start = NULL, *end = NULL;
   83 
   84   start = strchr(line, wrapchar);   // Locate first occurrence
   85   start++;                          // Advance one past it
   86   end = strchr(start, wrapchar);    // Locate last occurrence
   87 
   88   strncpy(buf, start, end - start); // Copy to the buffer
   89   buf[end - start] = '\0';          // Null terminate
   90   return buf;
   91 }
   92 
   93 /**
   94  * trim:
   95  * Function to trim all leading and trailing whitespace. Note that this mutates
   96  * the source string by writing a null byte over the first trailing whitespace.
   97  *
   98  * @str     String to trim
   99  *
  100  * @return  Pointer to the first non-whitespace character in str
  101  */
  102 char* trim(char* str) {
  103   int i = 0;
  104   char* start;
  105 
  106   // Move the cursor forward
  107   while(str[i] == ' ' || str[i] == '\t')
  108     i++;
  109   start = &str[i];
  110 
  111   // Reset i to end of string
  112   i = strlen(str) - 1;
  113   while(str[i] == ' ' || str[i] == '\t' || str[i] == '\n')
  114     i--;
  115 
  116   if(str[i] != '\0')
  117     str[i + 1] = '\0';
  118   return start;
  119 }
  120 
  121 /**
  122  * line_in_file:
  123  * Checks if the specified line is in the specified file. Note that this
  124  * exactly matches the two lines, so partial matches will still fail.
  125  *
  126  * @path    String path to the file to search
  127  * @line    Line to check if present in file
  128  *
  129  * @return  1 if present, 0 if not, -1 if file could not be accessed
  130  */
  131 int line_in_file(char* path, char* line) {
  132   FILE* fd = fopen(path, "r");
  133   char buf[256];
  134   int retval = 0;
  135 
  136   if(!fd) {
  137     fprintf(stderr, "Could not access %s\n", path);
  138     return -1;
  139   }
  140 
  141   while(fgets(buf, 256, fd)) {
  142     strcpy(buf, trim(buf));
  143     if(strcmp(buf, line) == 0) {
  144       retval = 1;
  145       break;
  146     }
  147   }
  148   fclose(fd);
  149   return retval;
  150 }
  151 
  152 
  153 /**
  154  * is_git_cmd:
  155  * Checks if input string is a valid git server command.
  156  *
  157  * @str     String to check if it is a valid git server command
  158  *
  159  * @return  1 if is a git command, 0 if not
  160  */
  161 int is_git_cmd(char* str) {
  162   if(strcmp(str, "git-upload-pack") == 0
  163     || strcmp(str, "git-upload-archive") == 0
  164     || strcmp(str, "git-receive-pack") == 0)
  165     return 1;
  166   return 0;
  167 }
  168 
  169 
  170 /**
  171  * is_allowed_cmd:
  172  * Checks the git-shell-commands directory for a filename matching the input
  173  * string str. Matching file must be executable to return a positive.
  174  *
  175  * @str     Command name
  176  *
  177  * @return  1 if allowed, 0 if not
  178  */
  179 int is_allowed_cmd(char* str) {
  180   char path[256];
  181   sprintf(path, "%s/git-shell-commands/%s", getenv("HOME"), str);
  182   if(access(path, F_OK) != -1 && access(path, X_OK) != -1)
  183     return 1;
  184   return 0;
  185 }
  186 
  187 
  188 /**
  189  * validate_git:
  190  * Validates the specified user's acccess to the git repo pointed to in the
  191  * SSH_ORIGINAL_COMMAND environment variable.
  192  *
  193  * @user    Name of the user for whom to check permission
  194  *
  195  * @return  1 if permitted, 0 if not
  196  */
  197 int validate_git(char* user) {
  198   char repopath[128];  // Buffer for the repo path (from SSH_ORIGINAL_COMMAND)
  199   char userspath[256]; // Path to the repo's users file (if one is specified)
  200 
  201   unwrap('\'', getenv("SSH_ORIGINAL_COMMAND"), repopath);
  202   if(strcmp(&repopath[strlen(repopath) - 4], ".git") == 0)
  203     sprintf(userspath, "%s/users", repopath);
  204   else
  205     sprintf(userspath, "%s.git/users", repopath);
  206 
  207   if(access(userspath, F_OK) == -1) {
  208     fprintf(stderr, "Repo %s does not exist or is misconfigured.\n", repopath);
  209     return 0;
  210   }
  211   if(line_in_file(userspath, user) != 1) {
  212     fprintf(stderr, "User %s does not have permission to access repo %s\n", user, repopath);
  213     return 0;
  214   }
  215   return 1;
  216 }
  217 
  218 
  219 int main(int argc, char* argv[]) {
  220   char cmd[128];   // Buffer for the first cmd in SSH_ORIGINAL_COMMAND
  221   char gitsh[256]; // Buffer for the git-shell cmd (from SSH_ORIGINAL_COMMAND)
  222   char* user;
  223   char msg[256];
  224 
  225   // Ensure username is specified
  226   if(argc == 1) {
  227     printf("ERROR: Username no specified in authorized_keys\n");
  228     return 1;
  229   }
  230   user = argv[1];
  231   // Read the USERNAME variable
  232   setenv("USERNAME", user, 1);
  233 
  234   // Ensure a command was specified
  235   if(! getenv("SSH_ORIGINAL_COMMAND")) {
  236     sprintf(msg, "[%s] logged in without specifying a command", user);
  237     logmsg(msg);
  238     printf("No soup for you!\n");
  239     return 1;
  240   }
  241 
  242   // Read the first command in the ssh
  243   sscanf(getenv("SSH_ORIGINAL_COMMAND"), "%s [^\n]", cmd);
  244 
  245   if(is_git_cmd(cmd)) {
  246     // Read the repo path (command argument)
  247     if(!validate_git(user)) {
  248       sprintf(msg, "[%s] attempted invalid git command \"%s\"",
  249           user, getenv("SSH_ORIGINAL_COMMAND"));
  250       logmsg(msg);
  251       return 1;
  252     }
  253   } else if(! is_allowed_cmd(cmd)) {
  254     sprintf(msg, "[%s] attempted disallowed command \"%s\"",
  255         user, getenv("SSH_ORIGINAL_COMMAND"));
  256     logmsg(msg);
  257     fprintf(stderr, "Command '%s' is not allowed\n", cmd);
  258     return 1;
  259   }
  260 
  261   sprintf(msg, "[%s] executed \"%s\"", user, getenv("SSH_ORIGINAL_COMMAND"));
  262   logmsg(msg);
  263   sprintf(gitsh, "/usr/bin/env git-shell -c \"%s\"", getenv("SSH_ORIGINAL_COMMAND"));
  264   system(gitsh);
  265 
  266   return 0;
  267 }

Generated by cgit