1 #!/usr/bin/env bash
2 #
3 # Gitaccess implements basic access controls for git.
4 # Copyright (C) 2017 Aaron Ball <nullspoon@oper.io>
5 #
6 # This program is free software; you can redistribute it and/or modify it under
7 # the terms of the GNU General Public License as published by the Free Software
8 # Foundation; either version 2 of the License, or (at your option) any later
9 # version.
10 #
11 # This program is distributed in the hope that it will be useful, but WITHOUT
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14 # details.
15 #
16 # You should have received a copy of the GNU General Public License along with
17 # this program; if not, write to the Free Software Foundation, Inc., 51
18 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 #
20 #
21 # Description
22 # -----------
23 #
24 # This script provides basic access controls to git repos. For this script to
25 # work, it requires that each repository has a 'users' file. This script reads
26 # that file and determines if the user associated with the logged in ssh key
27 # has access to that repo.
28 #
29 # This script also provides support for interactive git shell, interactive
30 # shell rejection via the no-interactive-shell script, and any other scripts
31 # that are placed inside the ~/git-shell-commands directory.
32 #
33 # To use this script for a specified ssh key, call it using the command
34 # directive in the ~/.ssh/authorized keys file using the following syntax
35 #
36 # # Key for user <username>
37 # command="gitaccess <username>" ecdsa-sha2-nistp521 AAAAE2v....
38 #
39
40
41 #
42 # Logging function for standardized log output. This also ensures that log
43 # messages don't cause perceived corruption in git responses, as they always
44 # report to stderr.
45 #
46 # @param lvl Log level string. Can be anything, but error, info, warn, fatal,
47 # etc. are recomended
48 # @param msg Log message
49 #
50 log() {
51 local lvl=${1}
52 shift
53 local msg=${@}
54
55 d=$(date '+%F %T')
56 printf "%s %s %s\n" "${d}" "${lvl}" "${msg[@]}" >> ~/git.log
57 printf "%s %s %s\n" "${d}" "${lvl}" "${msg[@]}" >&2
58 }
59
60 # Some logging macros to save typing time and space
61 lerror() { log 'error' ${@}; }
62 linfo() { log 'info ' ${@}; }
63 lwarn() { log 'warn ' ${@}; }
64 lwarn() { log 'fatal' ${@}; }
65
66
67 #
68 # Resolves the specified git repo path. Returns failure if...
69 # No repo exists at the path
70 # No repo exists at the path with .git appended
71 # The specified path exists, but is no a bare repository
72 #
73 # If the path is able to be resolved, the updated path is returned with an exit
74 # code of 0.
75 #
76 # @param repopath Path to the repo to resolve
77 #
78 git_resolve_path() {
79 local repopath=${1:-}
80 local isbare
81
82 # Resolve the path correctly if it has .git on the end that was not specified
83 [ -d ${repopath}.git ] && repopath=${repopath}.git
84 # If no repo can be found still, return failure
85 [ ! -d ${repopath} ] && lerror "No repo exists at ${repopath}" && return 1
86
87 isbare=$(git --git-dir="${repopath}" rev-parse --is-bare-repository 2>/dev/null)
88 [ "${isbare:-}" = 'true' ] && printf ${repopath} && return 0
89
90 lerror "Not a git repository: ${repopath}"
91 return 1
92 }
93
94
95 #
96 # Checks if the specified user has acccess to the specified repo.
97 # Requires the presence of the users file at the top level of the bare repo.
98 # This file should contain one username per line
99 #
100 # @param repopath Path to the repo we're checking access to
101 # @param user Username to check for access
102 #
103 git_check_access() {
104 local repopath=${1}
105 local user=${2}
106
107 local found=0 # Number of times the user is found in the users file
108
109 if [ -d ${repopath} ]; then
110 # Fail if users file is not found
111 if [ ! -f ${repopath}/users ]; then
112 lerror "No users file found in ${repopath}."
113 lerror "Access denied"
114 printf 0
115 return 1
116 fi
117
118 # Check if the user is in the users file
119 found=$(grep -c "^[ ]*${user}[ ]*$" ${repopath}/users)
120 if [ ${found} -eq 0 ]; then
121 lerror "Permission denied for ${user} to ${repopath}"
122 printf 0
123 else
124 linfo "Permission granted for ${user} to ${repopath}"
125 printf 1
126 fi
127 else
128 lerror "Could not find repo at ${repopath}."
129 return 3
130 fi
131 }
132
133
134 #
135 # Ye olde main
136 #
137 main() {
138 local user="${1:-}"
139
140 local repopath='none'
141
142 # Detect if someone tries to launch this script from this script, thus creating
143 # an infinite recursive loop spawning subshells.
144 if [ "${SSH_ORIGINAL_COMMAND:-}" = "$(basename ${0})" ]; then
145 log error "Blocking infinite recursion"
146 exit 1
147 fi
148
149 # Launch git interractive shell if no commands were specified
150 if [ -z "${SSH_ORIGINAL_COMMAND:-}" ]; then
151 /usr/bin/env git shell
152 return $?
153 fi
154
155 # If the user specified a git-* command, check if they have access to run it.
156 if [ "${SSH_ORIGINAL_COMMAND:0:4}" = 'git-' ]; then
157 # Parse the repo path out from the git command
158 repopath="$(echo ${SSH_ORIGINAL_COMMAND} | cut -d ' ' -f 2 | tr -d "'")"
159
160 # Resolve the repo path (in case it needs a .git or something else)
161 repopath=$(git_resolve_path "${repopath}")
162 [ $? -gt 0 ] && return 1
163
164 # Verify the user has access to this repo
165 allowed=$(git_check_access "${repopath}" "${user}")
166 [ ${allowed} -ne 1 ] && exit 1
167 fi
168
169 # All checks passed. Run the command
170 /usr/bin/env git shell -c "${SSH_ORIGINAL_COMMAND}"
171 }
172
173 main ${@}
|