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 }
|