1 /**
2 * Gitaccess implements basic access controls for git servers.
3 * Copyright (C) 2021 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 program provides basic access controls to git repos. For this program
24 * to work, it requires each repository to have a 'users' file. That file is
25 * read to determine if the user associated with the logged in ssh key has
26 * access to that repo.
27 *
28 * This program also supports the interactive git shell, interactive shell
29 * rejection via the no-interactive-shell script, and any other scripts that
30 * 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>" ssh-ed25519 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 * trim:
71 * Function to trim all leading and trailing whitespace. Note that this mutates
72 * the source string by writing a null byte over the first trailing whitespace.
73 *
74 * @str String to trim
75 *
76 * @return Pointer to the first non-whitespace character in str
77 */
78 char* trim(char* str) {
79 int i = 0;
80 char* start;
81
82 // Move the cursor forward
83 while(str[i] == ' ' || str[i] == '\t')
84 i++;
85 start = &str[i];
86
87 // Reset i to end of string
88 i = strlen(str) - 1;
89 while(str[i] == ' ' || str[i] == '\t' || str[i] == '\n')
90 i--;
91
92 if(str[i] != '\0')
93 str[i + 1] = '\0';
94 return start;
95 }
96
97 /**
98 * line_in_file:
99 * Checks if the specified line is in the specified file. Note that this
100 * exactly matches the two lines, so partial matches will still fail.
101 *
102 * @path String path to the file to search
103 * @line Line to check if present in file
104 *
105 * @return 1 if present, 0 if not, -1 if file could not be accessed
106 */
107 int line_in_file(char* path, char* line) {
108 FILE* fd = fopen(path, "r");
109 char buf[256];
110 int retval = 0;
111
112 if(!fd) {
113 fprintf(stderr, "Could not access %s\n", path);
114 return -1;
115 }
116
117 while(fgets(buf, 256, fd)) {
118 strcpy(buf, trim(buf));
119 if(strcmp(buf, line) == 0) {
120 retval = 1;
121 break;
122 }
123 }
124 fclose(fd);
125 return retval;
126 }
127
128
129 /**
130 * is_git_cmd:
131 * Checks if input string is a valid git server command.
132 *
133 * @str String to check if it is a valid git server command
134 *
135 * @return 1 if is a git command, 0 if not
136 */
137 int is_git_cmd(char* str) {
138 if(strcmp(str, "git-upload-pack") == 0
139 || strcmp(str, "git-upload-archive") == 0
140 || strcmp(str, "git-receive-pack") == 0)
141 return 1;
142 return 0;
143 }
144
145
146 /**
147 * is_allowed_cmd:
148 * Checks the git-shell-commands directory for a filename matching the input
149 * string str. Matching file must be executable to return a positive.
150 *
151 * @str Command name
152 *
153 * @return 1 if allowed, 0 if not
154 */
155 int is_allowed_cmd(char* str) {
156 char path[256];
157 sprintf(path, "%s/git-shell-commands/%s", getenv("HOME"), str);
158 if(access(path, F_OK) != -1 && access(path, X_OK) != -1)
159 return 1;
160 return 0;
161 }
162
163
164 /**
165 * validate_git:
166 * Validates the specified user's acccess to the git repo pointed to in the
167 * SSH_ORIGINAL_COMMAND environment variable.
168 *
169 * @user Name of the user for whom to check permission
170 *
171 * @return 1 if permitted, 0 if not
172 */
173 int validate_git(char *user, char *repo) {
174 char userspath[256]; // Path to the repo's users file (if one is specified)
175
176 if(strcmp(&repo[strlen(repo) - 4], ".git") == 0)
177 sprintf(userspath, "%s/users", repo);
178 else
179 sprintf(userspath, "%s.git/users", repo);
180
181 if(access(userspath, F_OK) == -1) {
182 fprintf(stderr, "Repo %s does not exist or is misconfigured.\n", repo);
183 return 0;
184 }
185 if(line_in_file(userspath, user) != 1) {
186 fprintf(stderr, "User %s does not have permission to access repo %s\n", user, repo);
187 return 0;
188 }
189 return 1;
190 }
191
192
193 int main(int argc, char* argv[]) {
194 char cmd[128]; // Buffer for the first cmd in SSH_ORIGINAL_COMMAND
195 char repo[128]; // Buffer for the repo path (from SSH_ORIGINAL_COMMAND)
196 char gitsh[512]; // Buffer for the git-shell cmd (from SSH_ORIGINAL_COMMAND)
197 char* user;
198 char msg[512];
199
200 // Ensure username is specified
201 if(argc == 1) {
202 printf("ERROR: Username no specified in authorized_keys\n");
203 return 1;
204 }
205 user = argv[1];
206 // Set the USERNAME environment variable
207 setenv("USERNAME", user, 1);
208
209 // Ensure a command was specified
210 if(! getenv("SSH_ORIGINAL_COMMAND")) {
211 sprintf(msg, "[%s] logged in without specifying a command", user);
212 logmsg(msg);
213 fprintf(stderr, "No soup for you!\n");
214 return 1;
215 }
216
217 // Read the first command in the ssh
218 sscanf(getenv("SSH_ORIGINAL_COMMAND"), "%128s '%128[^']'[^\n]", cmd, repo);
219
220 if(is_git_cmd(cmd)) {
221 // Read the repo path (command argument)
222 if(!validate_git(user, repo)) {
223 sprintf(msg, "[%s][%s] attempted invalid git command \"%s\"", \
224 user, repo, cmd);
225 logmsg(msg);
226 return 1;
227 }
228 } else if(! is_allowed_cmd(cmd)) {
229 sprintf(msg, "[%s][%s] attempted disallowed command \"%s\"", \
230 user, repo, cmd);
231 logmsg(msg);
232 fprintf(stderr, "Command '%s' is not allowed\n", cmd);
233 return 1;
234 }
235
236 sprintf(msg, "[%s][%s] executed \"%s\"", user, repo, cmd);
237 logmsg(msg);
238 sprintf(gitsh, "/usr/bin/env git-shell -c \"%s '%s'\"", cmd, repo);
239 system(gitsh);
240
241 return 0;
242 }
|