2011-02-11

Getting git accessible via smart http with LDAP integration

I work with a legacy development group which has been engulfed into a larger company. The legacy company was started in 2001 and at the time it was much faster to set up NIS/YP for our directory service as compared to LDAP, plus I also think that amd (our automounter which is also showing its age due to neglect by upstream) doesn't support LDAP maps. Well, I bet it is still much faster to set up NIS, but I would have a lot harder time justifying using the ancient NIS technology now. But in any case, the larger company uses LDAP and thus has a disconnected authentication system from us—which is very advantageous since we thus still have full administrative priviledges over our systems which would not be true if we were using their LDAP. However, this means that when someone in the other company needs access to our SCM we need to create local shell accounts for them. Tedious for us and I'm sure not fun for them due to the new password they need to remember. Some other day I might try to get a hybrid NIS/LDAP system running, but that day is not today.

In any case, today the situation came up again that someone needed access to our source. Initially I knew it would be read-only, so git-daemon was the obvious solution. So I created:

/etc/xinetd.d/git-daemon
# default: on
# description: The git server provides read-only git service for /git
service git
{
  disable = no
  socket_type = stream
  wait = no
  user = root
  log_on_success += USERID
  log_on_failure += USERID
  server = /usr/local/bin/git
  server_args = daemon --inetd --verbose --export-all /git
}

One `/etc/init.d/xinetd reload` later and I am all set. But…I know that sometime I am going to need to provide read-write access for this user. Creating another shell account is tedious as I already mentioned, so what could I do about this?

Well, Smart HTTP would seem to be the best solution. It uses the web server (Apache) for authentication and I have already explored doing LDAP integration with Apache+PHP, so that should be fairly straightforward. However, I've never really played with smart http yet, so it is a good learning experience. Well, to start I'll get read-only access going:

/etc/httpd/conf.d/git-http-backend.conf

SetEnv GIT_PROJECT_ROOT /git
SetEnv GIT_HTTP_EXPORT_ALL
ScriptAlias /git/ /usr/local/libexec/git-core/git-http-backend/

This is straight out of the git-http-backend man page, so I expected it to work. One `/etc/init.d/httpd reload` later and all is well. `git clone http://git/git/testrepo` works perfectly. Next is getting LDAP working. Forunately as I said I've done LDAP authentication with Apache before and during the development of that functionality I managed to connect to the larger company's LDAP server. But since they were using Active Directory and I had a lot of problems getting the DN for myself I just wanted to double-check my configuration.

shell> ldapsearch -x -W -H ldap://ldap -D 'CN=Seth Baka,CN=Users,DC=example,DC=com' -b "DC=example,DC=com"
ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)

Uh what? This was the exact command from my shell history (a 16k line HISTFILE is occasionally very useful) so how could it fail? I know my password is correct. Could they have zapped LDAP access from me? After various experiments from different machines and using different binding names, I came across a very informative webpage which said that you could stick your email address in for the bind DN so you don't have to know the exact syntax of your DN.

shell> ldapsearch -x -W -H ldap://ldap -D 'sethnobaka@example.com' -b "DC=example,DC=com"

# numResponses: 1002
# numEntries: 1000
# numReferences: 1

Success! Further investigation of the resulting LDIF file showed that they rearchitected their LDAP schema without telling me—very rude. Using the new DN for me I'm able to bind and I'm well on my way.

/etc/httpd/conf.d/git-http-backend.conf
SetEnv GIT_PROJECT_ROOT /git
SetEnv GIT_HTTP_EXPORT_ALL
ScriptAlias /git/ /usr/local/libexec/git-core/git-http-backend/
<locationmatch "^/git">
   AuthName "Git Repos"
   AuthType Basic
   AuthBasicProvider ldap
   AuthzLDAPAuthoritative off
   AuthLDAPUrl "ldap://ldap/dc=example,dc=com?sAMAccountName"
   AuthLDAPBindDN sethnobaka@example.com
   AuthLDAPBindPassword mypassword
   Require valid-user
</locationmatch>

Well, I have to say that having to store my personal LDAP password in an apache config file is pretty annoying. Can't apache check the authentication via a bind instead of binding first and then validating accounts and the like? Pretty annoying if you ask me, but the advantage is that multi-domain schemas get handled more automatically, so I can see some small benefit. In any case, I should be authenticating so lets see if we can clone again. After a quick `/etc/init.d/httpd reload` and another, `git clone http://git/git/testrepo`, I am prompted for my password (via an X popup) and then am able to clone. Huzzah!

Now lets try to push. It probably won't work, but the error should be informative. After I committed a tiny change, as expected the push fails.

shell> git push
error: unpack failed: unpack-objects abnormal exit

Looking in the httpd error log shows me:

error: insufficient permission for adding an object to repository database ./objects

Well, no surprise there. The apache user is not allowed to write into my git repository. I expected this, but at least not failing sooner is a good sign I guess. All I need to do is…hmm…I guess create a user in the correct group for apache-git integration and then use Apache's suexec to get it to run under the correct UID. No problem.

/etc/httpd/conf.d/git-http-backend.conf

SetEnv GIT_PROJECT_ROOT /git
SetEnv GIT_HTTP_EXPORT_ALL
SuexecUserGroup gitldap dev
ScriptAlias /git/ /usr/local/libexec/git-core/git-http-backend/
<locationmatch "^/git">
   AuthName "Git Repos"
   AuthType Basic
   AuthBasicProvider ldap
   AuthzLDAPAuthoritative off
   AuthLDAPUrl "ldap://ldap/dc=example,dc=com?sAMAccountName"
   AuthLDAPBindDN sethnobaka@example.com
   AuthLDAPBindPassword mypassword
   Require valid-user
</locationmatch>

Well OK, I actually find it pretty annoying that suexec cannot be constrained to a particular CGI or Directory as far as I can see, but it doesn't seem to matter yet, so off we go.

shell> /etc/init.d/httpd reload
shell> git push
error: The requested URL returned error: 500 while accessing http://git/git/testrepo/info/refs

I will not belabor the restarts and tests I did to track down the error, but suffice it to say that suexec is very anal about where the programs can go (/var/www for me) and whether or not they can be symlinks (hint, they cannot), and who the owner must be (why is root not a fine owner?). So in the end I had to write a wrapper shell script and modify the apache configuration:

/etc/httpd/conf.d/git-http-backend.conf

ScriptAlias /git/ /var/www/cgi-bin/git/git-http-backend/
SuexecUserGroup gitldap dev
<Directory /var/www/cgi-bin/git>
AllowOverride None
Options +ExecCGI
Require valid-user
</Directory>
<LocationMatch "^/git/">
AuthName "Git Repos"
AuthType Basic
AuthBasicProvider ldap
AuthzLDAPAuthoritative off
AuthLDAPUrl "ldap://ldap/dc=example,dc=com?sAMAccountName"
AuthLDAPBindDN sethnobaka@example.com
AuthLDAPBindPassword mypassword
Require valid-user
</LocationMatch>

/var/www/cgi-bin/git/git-http-backend
#!/bin/sh
export GIT_PROJECT_ROOT=/git GIT_HTTP_EXPORT_ALL=ALL
cd $GIT_PROJECT_ROOT
exec /usr/local/libexec/git-core/git-http-backend "$@"

Restart and try to push again as normal. Success! Now we finally are able to push, except…the email announcement is coming from the wrong email account due to the suexec user being used now instead of the normal user's shell account. Forging email in the post-receive script solves that problem. But the next problem is that gitweb stopped working, and it stopped working due to SuexecUserGroup. I still really don't understand why you are not allowed to restrict the SuexecUserGroup by location. Doing it by virtual hosts is a dull ax as opposed to a fine scalpel. I can't divide by virtual hosts here, so I need to do the same thing I did before: create a wrapper shell script in the right location. Of course, I have to use a different right location since I don't want the safety "Require valid-user" I used with cgi-bin/g/ to take effect, and even worse, I need to dance around the location of the static files.

/etc/httpd/conf.d/gitweb.conf
Alias /gitweb/static/ /var/www/html/gitweb/static/
ScriptAlias /gitweb/ /var/www/cgi-bin/gw/

/var/www/cgi-bin/gitweb.cgi
#!/bin/sh
exec /usr/local/share/gitweb/gitweb.cgi "$@"

Now, finally, everything seems to be working. This was more stupid than it should have been, so I hope that someone who need to fight the same stupidity can use this little guide.

No comments: