--- /dev/null
+#!/usr/bin/env python
+
+"""
+Cron-job that dumps files required in the filesystem to make pggit work.
+
+This means:
+
+~/.ssh/authorized_keys
+
+FIXME: I believe it should also contain the web/anon publishing stuff
+"""
+
+import sys
+import os
+import shutil
+import psycopg2
+import ConfigParser
+import urllib
+
+class AuthorizedKeysDumper:
+ def __init__(self, db, conf):
+ self.db = db
+ self.conf = conf
+
+ def dump(self):
+ self.dumpkeys()
+ self.dumprepos()
+
+ def dumpkeys(self):
+ # FIXME: use a trigger to indicate if *anything at all* has changed
+ curs = self.db.cursor()
+ curs.execute("SELECT userid,sshkey FROM git_users ORDER BY userid")
+ f = open("%s/.ssh/authorized_keys.tmp" % self.conf.get("paths", "githome"), "w")
+ for userid,sshkey in curs:
+ f.write("command=\"%s %s\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc %s\n" % (self.conf.get("paths", "pggit"), userid, sshkey))
+ f.close()
+ os.chmod("%s/.ssh/authorized_keys.tmp" % self.conf.get("paths", "githome"), 0600)
+ os.rename("%s/.ssh/authorized_keys.tmp" % self.conf.get("paths", "githome"), "%s/.ssh/authorized_keys" % self.conf.get("paths", "githome"))
+
+ def dumprepos(self):
+ # FIXME: use a trigger to indicate if *anything at all* has changed
+ curs = self.db.cursor()
+ curs.execute("SELECT name,anonymous,web FROM repositories WHERE approved ORDER BY name")
+ f = open("%s.tmp" % self.conf.get("paths", "gitweblist"), "w")
+ for name, anon, web in curs:
+ # Check if this repository exists at all
+ if not os.path.isdir("%s/repos/%s" % (self.conf.get("paths", "githome"), name)):
+ # Does not exist, let's initialize a new one
+ print "Initializing new git repository %s" % name
+ os.environ['GIT_DIR'] = "%s/repos/%s"% (self.conf.get("paths", "githome"), name)
+ os.system("git init --bare")
+ del os.environ['GIT_DIR']
+
+ # Check for publishing options here
+ if web:
+ f.write("%s\n" % (urllib.quote_plus(name)))
+ anonfile = "%s/repos/%s/git-daemon-export-ok" % (self.conf.get("paths", "githome"), name)
+ if anon:
+ if not os.path.isfile(anonfile):
+ open(anonfile, "w").close()
+ else:
+ if os.path.isfile(anonfile):
+ os.remove(anonfile)
+ f.close()
+ os.chmod("%s.tmp" % self.conf.get("paths", "gitweblist"), 0644)
+ os.rename("%s.tmp" % self.conf.get("paths", "gitweblist"), self.conf.get("paths", "gitweblist"))
+
+if __name__ == "__main__":
+ c = ConfigParser.ConfigParser()
+ c.read("pggit.settings")
+ db = psycopg2.connect(c.get('database','db'))
+ AuthorizedKeysDumper(db, c).dump()
+
--- /dev/null
+#!/usr/bin/env python
+
+"""
+Cron-job that synchronizes SSH public keys for all users that have them with the
+wwwmaster system.
+
+"""
+
+import sys
+import os
+import psycopg2
+import ConfigParser
+
+class KeySynchronizer:
+ def __init__(self, db):
+ self.db = db
+
+ def sync(self):
+ """
+ Perform the synchronization. This is going to be rather inefficient - we just
+ load up the complete list of users in memory, and then write it to a local table.
+
+ There's not likely (TM) to ever be a lot of data...
+ """
+ masterpg = psycopg2.connect(c.get('database','masterdb'))
+ curs = self.db.cursor()
+ mcurs = masterpg.cursor()
+
+ # Fetch last sync date, and see if anything has changed since
+ curs.execute("SELECT lastsync FROM key_last_sync LIMIT 1")
+ lastsync = curs.fetchone()[0]
+
+ mcurs.execute("SELECT CURRENT_TIMESTAMP, CASE WHEN EXISTS (SELECT * FROM users_keys WHERE sshkey_last_update >= %s) THEN 1 ELSE 0 END", [lastsync])
+ synctime, hasupd = mcurs.fetchone()
+ if hasupd == 0:
+ return # Nothing changed, just get out
+
+ # Fetch a list of all keys on the master server
+ mcurs.execute("SELECT userid, sshkey FROM users_keys")
+ allkeys = mcurs.fetchall()
+ mcurs.close()
+ masterpg.close()
+
+ # Load them into the local table
+ curs.execute("TRUNCATE TABLE git_users")
+ for row in allkeys:
+ curs.execute("INSERT INTO git_users (userid, sshkey) VALUES (%s,%s)", row)
+
+ # If there ever turns out to be a bunch, better analyze
+ curs.execute("ANALYZE git_users")
+
+ # Note the fact that we have synced (note that we use the timestamp value from the master,
+ # in case there is clock skew)
+ curs.execute("UPDATE key_last_sync SET lastsync=%s", [synctime])
+
+ self.db.commit()
+
+if __name__ == "__main__":
+ c = ConfigParser.ConfigParser()
+ c.read("pggit.settings")
+ db = psycopg2.connect(c.get('database','db'))
+ KeySynchronizer(db).sync()
+
--- /dev/null
+#!/usr/bin/env python
+
+"""
+Hook to be called when attempting to access the git repository through
+ssh.
+
+Verify permissions and pass control to git-shell if allowed.
+
+First commandline argument should contain the username to authenticate,
+which is controlled by ~/.ssh/authorized_keys
+
+SVN_ORIGINAL_COMMAND contains the git command and argument, which is
+controlled by the client side git command.
+"""
+
+import sys
+import os
+import psycopg2
+
+# MUST have trailing slash
+REPOPREFIX="/home/gitlab/"
+
+ALLOWED_COMMANDS = ('git-upload-pack', 'git-receive-pack')
+WRITE_COMMANDS = ('git-receive-pack')
+
+class Logger:
+ def __init__(self):
+ self.user = "Unknown"
+
+ def log(self, message):
+ f = open("/home/gitlab/pggit.log","a")
+ f.write("(%s): %s" % (self.user, message))
+ f.write("\n")
+ f.close()
+
+ def setuser(self, user):
+ if user:
+ self.user = user
+
+class PgGit:
+ user = None
+ command = None
+ path = None
+ subpath = None
+
+ def __init__(self):
+ self.logger = Logger()
+ pass
+
+ def parse_commandline(self):
+ if len(sys.argv) != 2:
+ raise Exception("Can only be run with one commandline argument!")
+ self.user = sys.argv[1]
+ self.logger.setuser(self.user)
+
+ def parse_command(self):
+ env = os.environ.get('SSH_ORIGINAL_COMMAND', None)
+ if not env:
+ raise Exception("No SSH_ORIGINAL_COMMAND present!")
+
+ # env contains "git-<command> <argument>" or "git <command> <argument>"
+ command, args = env.split(None, 1)
+ if command == "git":
+ subcommand, args = args.split(None,1)
+ command = "git-%s" % subcommand
+ if not command in ALLOWED_COMMANDS:
+ raise Exception("Command '%s' not allowed" % command)
+
+ self.command = command
+ if not args.startswith("'/"):
+ raise Exception("Expected git path to start with slash!")
+
+ # FIXME: what about that single quote? Make sure it's there?
+
+ # use os.path.normpath to make sure the user does not attempt to break out of the repository root
+ self.path = os.path.normpath(("%s%s" % (REPOPREFIX, args[2:].rstrip("'"))))
+ if not self.path.startswith(REPOPREFIX):
+ raise Exception("Escaping the root directory is of course not permitted")
+ if not os.path.exists(self.path):
+ raise Exception('git repository "%s" does not exist' % args)
+ self.subpath = self.path[len(REPOPREFIX):]
+
+ def check_permissions(self):
+ writeperm = False
+ db = psycopg2.connect("dbname=gitlab host=/tmp/ user=mha")
+ curs = db.cursor()
+ curs.execute("SELECT write FROM repository_permissions INNER JOIN repositories ON repoid=repository WHERE userid=%s AND name=%s",
+ (self.user, self.subpath))
+ try:
+ writeperm = curs.fetchone()[0]
+ except:
+ raise Exception("Permission denied on repository for user %s" % self.user)
+
+ if self.command in WRITE_COMMANDS:
+ if not writeperm:
+ raise Exception("Write permission denied on repository for user %s" % self.user)
+
+
+ def run_command(self):
+ self.logger.log("Running \"git shell %s %s\"" % (self.command, "'%s'" % self.path))
+ os.execvp('git', ['git', 'shell', '-c', "%s %s" % (self.command, "'%s'" % self.path) ])
+
+ def run(self):
+ try:
+ self.parse_commandline()
+ self.parse_command()
+ self.check_permissions()
+ self.run_command()
+ except Exception, e:
+ try:
+ self.logger.log(e)
+ except Exception, e:
+ # If we failed to log, try once more with a new logger, otherwise,
+ # just accept that we failed.
+ try:
+ Logger().log(e)
+ except:
+ pass
+ raise e
+
+if __name__ == "__main__":
+ PgGit().run()
+
--- /dev/null
+[database]
+db=dbname=pggit host=/tmp/ user=mha
+;masterdb=hostname=wwwmaster.postgresql.org dbname=186_www user=auth_svc
+masterdb=dbname=186_www host=/tmp/ user=mha
+
+[user]
+user=pggit
+group=pggit
+
+[paths]
+githome=/home/gitlab
+pggit=/opt/pgsql/pggit/pggit.py
+gitweblist=/opt/pgsql/pggit/__temp__gitweb.list
--- /dev/null
+-- Assume a view 'git_users' that has the columns userid and sshkey
+
+CREATE TABLE repositories(
+ repoid SERIAL NOT NULL PRIMARY KEY,
+ name varchar(64) NOT NULL UNIQUE,
+ description text NOT NULL,
+ anonymous bool NOT NULL default 'f',
+ web bool NOT NULL default 'f',
+ approved bool NOT NULL DEFAULT 'f'
+);
+
+CREATE TABLE repository_permissions (
+ id SERIAL NOT NULL PRIMARY KEY,
+ repository int NOT NULL REFERENCES repositories(repoid),
+ userid text NOT NULL, -- intentionally not putting a foreign key here
+ level int NOT NULL DEFAULT 0,
+ CONSTRAINT levelcheck CHECK (level IN (0,1,2))
+);
+
+CREATE UNIQUE INDEX idx_repo_perm_rep_uid ON repository_permissions (repository, userid);
+
+
+-- This is where we store the synchronized keys
+CREATE TABLE git_users(userid text PRIMARY KEY, sshkey text);
+