git-serve: Close orphan requests upon disown
[aur.git] / aurweb / git / serve.py
blob476aea864721d272cecbdc3ae0b4de0442e9a62c
1 #!/usr/bin/python3
3 import os
4 import re
5 import shlex
6 import subprocess
7 import sys
8 import time
10 import aurweb.config
11 import aurweb.db
13 notify_cmd = aurweb.config.get('notifications', 'notify-cmd')
15 repo_path = aurweb.config.get('serve', 'repo-path')
16 repo_regex = aurweb.config.get('serve', 'repo-regex')
17 git_shell_cmd = aurweb.config.get('serve', 'git-shell-cmd')
18 git_update_cmd = aurweb.config.get('serve', 'git-update-cmd')
19 ssh_cmdline = aurweb.config.get('serve', 'ssh-cmdline')
21 enable_maintenance = aurweb.config.getboolean('options', 'enable-maintenance')
22 maintenance_exc = aurweb.config.get('options', 'maintenance-exceptions').split()
25 def pkgbase_from_name(pkgbase):
26 conn = aurweb.db.Connection()
27 cur = conn.execute("SELECT ID FROM PackageBases WHERE Name = ?", [pkgbase])
29 row = cur.fetchone()
30 return row[0] if row else None
33 def pkgbase_exists(pkgbase):
34 return pkgbase_from_name(pkgbase) is not None
37 def list_repos(user):
38 conn = aurweb.db.Connection()
40 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
41 userid = cur.fetchone()[0]
42 if userid == 0:
43 die('{:s}: unknown user: {:s}'.format(action, user))
45 cur = conn.execute("SELECT Name, PackagerUID FROM PackageBases " +
46 "WHERE MaintainerUID = ?", [userid])
47 for row in cur:
48 print((' ' if row[1] else '*') + row[0])
49 conn.close()
52 def create_pkgbase(pkgbase, user):
53 if not re.match(repo_regex, pkgbase):
54 die('{:s}: invalid repository name: {:s}'.format(action, pkgbase))
55 if pkgbase_exists(pkgbase):
56 die('{:s}: package base already exists: {:s}'.format(action, pkgbase))
58 conn = aurweb.db.Connection()
60 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
61 userid = cur.fetchone()[0]
62 if userid == 0:
63 die('{:s}: unknown user: {:s}'.format(action, user))
65 now = int(time.time())
66 cur = conn.execute("INSERT INTO PackageBases (Name, SubmittedTS, " +
67 "ModifiedTS, SubmitterUID, MaintainerUID) VALUES " +
68 "(?, ?, ?, ?, ?)", [pkgbase, now, now, userid, userid])
69 pkgbase_id = cur.lastrowid
71 cur = conn.execute("INSERT INTO PackageNotifications " +
72 "(PackageBaseID, UserID) VALUES (?, ?)",
73 [pkgbase_id, userid])
75 conn.commit()
76 conn.close()
79 def pkgbase_adopt(pkgbase, user, privileged):
80 pkgbase_id = pkgbase_from_name(pkgbase)
81 if not pkgbase_id:
82 die('{:s}: package base not found: {:s}'.format(action, pkgbase))
84 conn = aurweb.db.Connection()
86 cur = conn.execute("SELECT ID FROM PackageBases WHERE ID = ? AND " +
87 "MaintainerUID IS NULL", [pkgbase_id])
88 if not privileged and not cur.fetchone():
89 die('{:s}: permission denied: {:s}'.format(action, user))
91 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
92 userid = cur.fetchone()[0]
93 if userid == 0:
94 die('{:s}: unknown user: {:s}'.format(action, user))
96 cur = conn.execute("UPDATE PackageBases SET MaintainerUID = ? " +
97 "WHERE ID = ?", [userid, pkgbase_id])
99 cur = conn.execute("SELECT COUNT(*) FROM PackageNotifications WHERE " +
100 "PackageBaseID = ? AND UserID = ?",
101 [pkgbase_id, userid])
102 if cur.fetchone()[0] == 0:
103 cur = conn.execute("INSERT INTO PackageNotifications " +
104 "(PackageBaseID, UserID) VALUES (?, ?)",
105 [pkgbase_id, userid])
106 conn.commit()
108 subprocess.Popen((notify_cmd, 'adopt', str(pkgbase_id), str(userid)))
110 conn.close()
113 def pkgbase_get_comaintainers(pkgbase):
114 conn = aurweb.db.Connection()
116 cur = conn.execute("SELECT UserName FROM PackageComaintainers " +
117 "INNER JOIN Users " +
118 "ON Users.ID = PackageComaintainers.UsersID " +
119 "INNER JOIN PackageBases " +
120 "ON PackageBases.ID = PackageComaintainers.PackageBaseID " +
121 "WHERE PackageBases.Name = ? " +
122 "ORDER BY Priority ASC", [pkgbase])
124 return [row[0] for row in cur.fetchall()]
127 def pkgbase_set_comaintainers(pkgbase, userlist, user, privileged):
128 pkgbase_id = pkgbase_from_name(pkgbase)
129 if not pkgbase_id:
130 die('{:s}: package base not found: {:s}'.format(action, pkgbase))
132 if not privileged and not pkgbase_has_full_access(pkgbase, user):
133 die('{:s}: permission denied: {:s}'.format(action, user))
135 conn = aurweb.db.Connection()
137 userlist_old = set(pkgbase_get_comaintainers(pkgbase))
139 uids_old = set()
140 for olduser in userlist_old:
141 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?",
142 [olduser])
143 userid = cur.fetchone()[0]
144 if userid == 0:
145 die('{:s}: unknown user: {:s}'.format(action, user))
146 uids_old.add(userid)
148 uids_new = set()
149 for newuser in userlist:
150 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?",
151 [newuser])
152 userid = cur.fetchone()[0]
153 if userid == 0:
154 die('{:s}: unknown user: {:s}'.format(action, user))
155 uids_new.add(userid)
157 uids_add = uids_new - uids_old
158 uids_rem = uids_old - uids_new
160 i = 1
161 for userid in uids_new:
162 if userid in uids_add:
163 cur = conn.execute("INSERT INTO PackageComaintainers " +
164 "(PackageBaseID, UsersID, Priority) " +
165 "VALUES (?, ?, ?)", [pkgbase_id, userid, i])
166 subprocess.Popen((notify_cmd, 'comaintainer-add', str(pkgbase_id),
167 str(userid)))
168 else:
169 cur = conn.execute("UPDATE PackageComaintainers " +
170 "SET Priority = ? " +
171 "WHERE PackageBaseID = ? AND UsersID = ?",
172 [i, pkgbase_id, userid])
173 i += 1
175 for userid in uids_rem:
176 cur = conn.execute("DELETE FROM PackageComaintainers " +
177 "WHERE PackageBaseID = ? AND UsersID = ?",
178 [pkgbase_id, userid])
179 subprocess.Popen((notify_cmd, 'comaintainer-remove',
180 str(pkgbase_id), str(userid)))
182 conn.commit()
183 conn.close()
186 def pkgreq_by_pkgbase(pkgbase_id, reqtype):
187 conn = aurweb.db.Connection()
189 cur = conn.execute("SELECT PackageRequests.ID FROM PackageRequests " +
190 "INNER JOIN RequestTypes ON " +
191 "RequestTypes.ID = PackageRequests.ReqTypeID " +
192 "WHERE PackageRequests.Status = 0 " +
193 "AND PackageRequests.PackageBaseID = ?" +
194 "AND RequestTypes.Name = ?", [pkgbase_id, reqtype])
196 return [row[0] for row in cur.fetchall()]
199 def pkgreq_close(reqid, reason, comments, autoclose=False):
200 statusmap = {'accepted': 2, 'rejected': 3}
201 if reason not in statusmap:
202 die('{:s}: invalid reason: {:s}'.format(action, reason))
203 status = statusmap[reason]
205 conn = aurweb.db.Connection()
207 if autoclose:
208 userid = 0
209 else:
210 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
211 userid = cur.fetchone()[0]
212 if userid == 0:
213 die('{:s}: unknown user: {:s}'.format(action, user))
215 conn.execute("UPDATE PackageRequests SET Status = ?, ClosureComment = ? " +
216 "WHERE ID = ?", [status, comments, reqid])
217 conn.commit()
218 conn.close()
220 subprocess.Popen((notify_cmd, 'request-close', str(userid), str(reqid),
221 reason)).wait()
224 def pkgbase_disown(pkgbase, user, privileged):
225 pkgbase_id = pkgbase_from_name(pkgbase)
226 if not pkgbase_id:
227 die('{:s}: package base not found: {:s}'.format(action, pkgbase))
229 initialized_by_owner = pkgbase_has_full_access(pkgbase, user)
230 if not privileged and not initialized_by_owner:
231 die('{:s}: permission denied: {:s}'.format(action, user))
233 # TODO: Support disowning package bases via package request.
235 # Scan through pending orphan requests and close them.
236 comment = 'The user {:s} disowned the package.'.format(user)
237 for reqid in pkgreq_by_pkgbase(pkgbase_id, 'orphan'):
238 pkgreq_close(reqid, 'accepted', comment, True)
240 comaintainers = []
241 new_maintainer_userid = None
243 conn = aurweb.db.Connection()
245 # Make the first co-maintainer the new maintainer, unless the action was
246 # enforced by a Trusted User.
247 if initialized_by_owner:
248 comaintainers = pkgbase_get_comaintainers(pkgbase)
249 if len(comaintainers) > 0:
250 new_maintainer = comaintainers[0]
251 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?",
252 [new_maintainer])
253 new_maintainer_userid = cur.fetchone()[0]
254 comaintainers.remove(new_maintainer)
256 pkgbase_set_comaintainers(pkgbase, comaintainers, user, privileged)
257 cur = conn.execute("UPDATE PackageBases SET MaintainerUID = ? " +
258 "WHERE ID = ?", [new_maintainer_userid, pkgbase_id])
260 conn.commit()
262 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
263 userid = cur.fetchone()[0]
264 if userid == 0:
265 die('{:s}: unknown user: {:s}'.format(action, user))
267 subprocess.Popen((notify_cmd, 'disown', str(pkgbase_id), str(userid)))
269 conn.close()
272 def pkgbase_set_keywords(pkgbase, keywords):
273 pkgbase_id = pkgbase_from_name(pkgbase)
274 if not pkgbase_id:
275 die('{:s}: package base not found: {:s}'.format(action, pkgbase))
277 conn = aurweb.db.Connection()
279 conn.execute("DELETE FROM PackageKeywords WHERE PackageBaseID = ?",
280 [pkgbase_id])
281 for keyword in keywords:
282 conn.execute("INSERT INTO PackageKeywords (PackageBaseID, Keyword) " +
283 "VALUES (?, ?)", [pkgbase_id, keyword])
285 conn.commit()
286 conn.close()
289 def pkgbase_has_write_access(pkgbase, user):
290 conn = aurweb.db.Connection()
292 cur = conn.execute("SELECT COUNT(*) FROM PackageBases " +
293 "LEFT JOIN PackageComaintainers " +
294 "ON PackageComaintainers.PackageBaseID = PackageBases.ID " +
295 "INNER JOIN Users " +
296 "ON Users.ID = PackageBases.MaintainerUID " +
297 "OR PackageBases.MaintainerUID IS NULL " +
298 "OR Users.ID = PackageComaintainers.UsersID " +
299 "WHERE Name = ? AND Username = ?", [pkgbase, user])
300 return cur.fetchone()[0] > 0
303 def pkgbase_has_full_access(pkgbase, user):
304 conn = aurweb.db.Connection()
306 cur = conn.execute("SELECT COUNT(*) FROM PackageBases " +
307 "INNER JOIN Users " +
308 "ON Users.ID = PackageBases.MaintainerUID " +
309 "WHERE Name = ? AND Username = ?", [pkgbase, user])
310 return cur.fetchone()[0] > 0
313 def die(msg):
314 sys.stderr.write("{:s}\n".format(msg))
315 exit(1)
318 def die_with_help(msg):
319 die(msg + "\nTry `{:s} help` for a list of commands.".format(ssh_cmdline))
322 def warn(msg):
323 sys.stderr.write("warning: {:s}\n".format(msg))
326 def usage(cmds):
327 sys.stderr.write("Commands:\n")
328 colwidth = max([len(cmd) for cmd in cmds.keys()]) + 4
329 for key in sorted(cmds):
330 sys.stderr.write(" " + key.ljust(colwidth) + cmds[key] + "\n")
331 exit(0)
334 def main():
335 user = os.environ.get('AUR_USER')
336 privileged = (os.environ.get('AUR_PRIVILEGED', '0') == '1')
337 ssh_cmd = os.environ.get('SSH_ORIGINAL_COMMAND')
338 ssh_client = os.environ.get('SSH_CLIENT')
340 if not ssh_cmd:
341 die_with_help("Interactive shell is disabled.")
342 cmdargv = shlex.split(ssh_cmd)
343 action = cmdargv[0]
344 remote_addr = ssh_client.split(' ')[0] if ssh_client else None
346 if enable_maintenance:
347 if remote_addr not in maintenance_exc:
348 die("The AUR is down due to maintenance. We will be back soon.")
350 if action == 'git' and cmdargv[1] in ('upload-pack', 'receive-pack'):
351 action = action + '-' + cmdargv[1]
352 del cmdargv[1]
354 if action == 'git-upload-pack' or action == 'git-receive-pack':
355 if len(cmdargv) < 2:
356 die_with_help("{:s}: missing path".format(action))
358 path = cmdargv[1].rstrip('/')
359 if not path.startswith('/'):
360 path = '/' + path
361 if not path.endswith('.git'):
362 path = path + '.git'
363 pkgbase = path[1:-4]
364 if not re.match(repo_regex, pkgbase):
365 die('{:s}: invalid repository name: {:s}'.format(action, pkgbase))
367 if action == 'git-receive-pack' and pkgbase_exists(pkgbase):
368 if not privileged and not pkgbase_has_write_access(pkgbase, user):
369 die('{:s}: permission denied: {:s}'.format(action, user))
371 os.environ["AUR_USER"] = user
372 os.environ["AUR_PKGBASE"] = pkgbase
373 os.environ["GIT_NAMESPACE"] = pkgbase
374 cmd = action + " '" + repo_path + "'"
375 os.execl(git_shell_cmd, git_shell_cmd, '-c', cmd)
376 elif action == 'set-keywords':
377 if len(cmdargv) < 2:
378 die_with_help("{:s}: missing repository name".format(action))
379 pkgbase_set_keywords(cmdargv[1], cmdargv[2:])
380 elif action == 'list-repos':
381 if len(cmdargv) > 1:
382 die_with_help("{:s}: too many arguments".format(action))
383 list_repos(user)
384 elif action == 'setup-repo':
385 if len(cmdargv) < 2:
386 die_with_help("{:s}: missing repository name".format(action))
387 if len(cmdargv) > 2:
388 die_with_help("{:s}: too many arguments".format(action))
389 warn('{:s} is deprecated. '
390 'Use `git push` to create new repositories.'.format(action))
391 create_pkgbase(cmdargv[1], user)
392 elif action == 'restore':
393 if len(cmdargv) < 2:
394 die_with_help("{:s}: missing repository name".format(action))
395 if len(cmdargv) > 2:
396 die_with_help("{:s}: too many arguments".format(action))
398 pkgbase = cmdargv[1]
399 if not re.match(repo_regex, pkgbase):
400 die('{:s}: invalid repository name: {:s}'.format(action, pkgbase))
402 if pkgbase_exists(pkgbase):
403 die('{:s}: package base exists: {:s}'.format(action, pkgbase))
404 create_pkgbase(pkgbase, user)
406 os.environ["AUR_USER"] = user
407 os.environ["AUR_PKGBASE"] = pkgbase
408 os.execl(git_update_cmd, git_update_cmd, 'restore')
409 elif action == 'adopt':
410 if len(cmdargv) < 2:
411 die_with_help("{:s}: missing repository name".format(action))
412 if len(cmdargv) > 2:
413 die_with_help("{:s}: too many arguments".format(action))
415 pkgbase = cmdargv[1]
416 pkgbase_adopt(pkgbase, user, privileged)
417 elif action == 'disown':
418 if len(cmdargv) < 2:
419 die_with_help("{:s}: missing repository name".format(action))
420 if len(cmdargv) > 2:
421 die_with_help("{:s}: too many arguments".format(action))
423 pkgbase = cmdargv[1]
424 pkgbase_disown(pkgbase, user, privileged)
425 elif action == 'set-comaintainers':
426 if len(cmdargv) < 2:
427 die_with_help("{:s}: missing repository name".format(action))
429 pkgbase = cmdargv[1]
430 userlist = cmdargv[2:]
431 pkgbase_set_comaintainers(pkgbase, userlist, user, privileged)
432 elif action == 'help':
433 cmds = {
434 "adopt <name>": "Adopt a package base.",
435 "disown <name>": "Disown a package base.",
436 "help": "Show this help message and exit.",
437 "list-repos": "List all your repositories.",
438 "restore <name>": "Restore a deleted package base.",
439 "set-comaintainers <name> [...]": "Set package base co-maintainers.",
440 "set-keywords <name> [...]": "Change package base keywords.",
441 "setup-repo <name>": "Create a repository (deprecated).",
442 "git-receive-pack": "Internal command used with Git.",
443 "git-upload-pack": "Internal command used with Git.",
445 usage(cmds)
446 else:
447 die_with_help("invalid command: {:s}".format(action))
450 if __name__ == '__main__':
451 main()