- commit
- 4c226c58ec59049b0b6ae7e4d030b1d8bef8fd16
- parent
- e05fedbb32c049ebefea6c9e8b47da20144ad21d
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2020-04-23 06:50
rm RW+ distinction
Diffstat
M | Makefile | 1 | - |
M | README.md | 30 | +++++++++++------------------- |
M | gitolite/shell.py | 8 | ++++---- |
M | gitolite/stagit.py | 83 | ++++++++++++++++++++++++++----------------------------------- |
D | gitolite/update.py | 38 | -------------------------------------- |
5 files changed, 50 insertions, 110 deletions
diff --git a/Makefile b/Makefile
@@ -21,7 +21,6 @@ install: stagit 21 21 install -D -m 644 README.md "${DESTDIR}/usr/share/doc/stagit/README.md" 22 22 23 23 install -D -m 755 gitolite/shell.py "${DESTDIR}/usr/lib/stagit/shell"24 -1 install -D -m 755 gitolite/update.py "${DESTDIR}/usr/lib/stagit/hooks/update"25 24 install -D -m 755 gitolite/post-update.py "${DESTDIR}/usr/lib/stagit/hooks/post-update" 26 25 install -D -m 644 gitolite/stagit.py "${DESTDIR}/usr/lib/python3/dist-packages/stagit.py" 27 26
diff --git a/README.md b/README.md
@@ -5,13 +5,10 @@ combined with access control scripts inspired by 5 5 [gitolite](https://gitolite.com/gitolite/). Together they are a simple yet 6 6 powerful solution for hosting git repositories. 7 78 -1 - All (git-)users use the same (ssh-)user.9 -1 - Access to repos is controlled using two scripts:10 -1 - one is called via the `command` option in `~/.ssh/authorized_keys`11 -1 - the second one is called via the `update` git hook12 -1 - A third script is called via the `post-update` git hook. If a public13 -1 repository was created/updated, a corresponding static website is14 -1 created/updated automatically.-1 8 - SSH Access to repos is controlled using the `command` option in -1 9 `~/.ssh/authorized_keys`. All (git-)users use the same (ssh-)user. -1 10 - The `post-update` git hook is used to automatically create/update a static -1 11 websites for public repositories. 15 12 16 13 ## Installation and setup 17 14 @@ -31,30 +28,25 @@ Then setup access control: 31 28 32 29 ## stagit.conf 33 3034 -1 The conf file is inspired by the [corresponding file in35 -1 gitolite](https://gitolite.com/gitolite/conf.html), but has a more INI-like36 -1 structure.37 -138 31 ``` 39 32 [GROUPS] 40 33 @staff = dilbert alice 41 34 42 35 [private]43 -1 !RW+ = admin-1 36 ssh = admin 44 37 45 38 [example] 46 39 desc = my shiny new project47 -1 !RW+ = @staff48 -1 !R = @all @public-1 40 ssh = @staff -1 41 http = yes 49 42 ``` 50 43 51 44 Every section defines one repo, except for the special `GROUPS` section which52 -1 can be used to define groups of users. Groups are prefixed with a `@`. Access53 -1 permissions are prefixed with a `!`. `RW+` controls read, write, and full54 -1 access. Full access includes force-pushing and creating tags.-1 45 can be used to define groups of users. The special group `@all` matches all -1 46 users. 55 4756 -1 There are two special groups: `@all` matches all users. `@public` enables57 -1 anonymous access via website and-1 48 The `ssh` key controls which users can access the repositories via ssh. The -1 49 `http` key is boolean and enables anonymous access via website and 58 50 [git-daemon](https://git-scm.com/book/en/v2/Git-on-the-Server-Git-Daemon). 59 51 60 52 ## Authorized keys
diff --git a/gitolite/shell.py b/gitolite/shell.py
@@ -29,14 +29,14 @@ if __name__ == '__main__': 29 29 user = sys.argv[1] 30 30 31 31 if 'SSH_ORIGINAL_COMMAND' not in os.environ:32 -1 for repo in stagit.config.iter_user_repos(user):33 -1 print(repo)-1 32 for repo in stagit.config.iter_repos(): -1 33 if stagit.config.can_ssh(repo, user): -1 34 print(repo) 34 35 sys.exit(0) 35 36 36 37 cmd, repo = parse_soc() 37 3838 -1 access = 'w' if cmd == 'git-receive-pack' else 'r'39 -1 if not stagit.config.has_perm(repo, user, access):-1 39 if not stagit.config.can_ssh(repo, user): 40 40 sys.stderr.write('access denied\n') 41 41 sys.exit(1) 42 42
diff --git a/gitolite/stagit.py b/gitolite/stagit.py
@@ -11,7 +11,6 @@ WWW_DIR = '/var/www/git/' 11 11 REPO_DIR = os.path.expanduser('~/repos/') 12 12 CONF_PATH = os.path.expanduser('~/stagit.conf') 13 1314 -1 UPDATE_HOOK = '/usr/lib/stagit/hooks/update'15 14 POST_UPDATE_HOOK = '/usr/lib/stagit/hooks/post-update' 16 15 17 16 @@ -32,49 +31,46 @@ class Config: 32 31 def get(self, section, key): 33 32 return self.config.get(section, key, fallback='') 34 3335 -1 def _iter_sets(self, repo, prefix):-1 34 def getboolean(self, section, key): -1 35 return self.config.getboolean(section, key, fallback=False) -1 36 -1 37 def can_ssh(self, repo, user): -1 38 users = set([user, '@all']) 36 39 try:37 -1 for key, value in self.config[repo].items():38 -1 if key[0] == prefix:39 -1 yield key, set(value.split())-1 40 for group, value in self.config['GROUPS'].items(): -1 41 if user in value.split(): -1 42 users.add(group) 40 43 except KeyError: 41 44 pass42 -143 -1 def has_perm(self, repo, user, access):44 -1 users = set([user, '@all'])45 -1 for group, values in self._iter_sets('GROUPS', '@'):46 -1 if user in values:47 -1 users.add(group)48 -1 for perm, values in self._iter_sets(repo, '!'):49 -1 if access in perm and not users.isdisjoint(values):50 -1 return True51 -1 return False52 -153 -1 def is_public(self, repo):54 -1 return self.has_perm(repo, '@public', 'r')-1 45 allowed = self.get(repo, 'ssh').split() -1 46 return not users.isdisjoint(allowed) 55 47 56 48 def iter_repos(self): 57 49 for key in self.config:58 -1 if key[0] != '@' and key not in ['GROUPS', 'DEFAULT']:-1 50 if key not in ['GROUPS', 'DEFAULT']: 59 51 yield key 60 5261 -1 def iter_user_repos(self, user):62 -1 for repo in self.iter_repos():63 -1 if self.has_perm(repo, user, 'r'):64 -1 yield repo65 -166 53 67 54 config = Config() 68 55 69 5670 -1 def update_metadata(repo):-1 57 def update_repo(repo): 71 58 repodir = get_repo_dir(repo) -1 59 if not os.path.exists(repodir): -1 60 os.makedirs(repodir, exist_ok=True) -1 61 subprocess.check_call(['git', 'init', '--bare'], cwd=repodir) -1 62 72 63 export_ok = os.path.join(repodir, 'git-daemon-export-ok')73 -1 if config.is_public(repo):-1 64 post_update = os.path.join(repodir, 'hooks', 'post-update') -1 65 if config.getboolean(repo, 'http'): -1 66 shutil.copy(POST_UPDATE_HOOK, post_update) 74 67 with open(export_ok, 'a'): 75 68 os.utime(export_ok, None)76 -1 elif os.path.exists(export_ok):77 -1 os.unlink(export_ok)-1 69 else: -1 70 if os.path.exists(export_ok): -1 71 os.unlink(export_ok) -1 72 if os.path.exists(post_update): -1 73 os.unlink(post_update) 78 74 79 75 with open(os.path.join(repodir, 'description'), 'w') as fh: 80 76 fh.write(config.get(repo, 'desc')) @@ -82,16 +78,6 @@ def update_metadata(repo): 82 78 fh.write(config.get(repo, 'url')) 83 79 84 8085 -1 def update_repo(repo):86 -1 path = get_repo_dir(repo)87 -1 if not os.path.exists(path):88 -1 os.makedirs(path, exist_ok=True)89 -1 subprocess.check_call(['git', 'init', '--bare'], cwd=path)90 -1 shutil.copy(UPDATE_HOOK, os.path.join(path, 'hooks', 'update'))91 -1 shutil.copy(POST_UPDATE_HOOK, os.path.join(path, 'hooks', 'post-update'))92 -1 update_metadata(repo)93 -194 -195 81 def render_index(): 96 82 fh = open(os.path.join(WWW_DIR, 'index.html'), 'w') 97 83 fh.write('<!DOCTYPE html>\n') @@ -107,20 +93,20 @@ def render_index(): 107 93 fh.write('<table>\n<thead>\n') 108 94 fh.write('<tr><th>Name</th><th>Description</th></tr>\n') 109 95 fh.write('</thead>\n<tbody>\n')110 -1 for repo in config.iter_user_repos('@public'):111 -1 fh.write('<tr><td><a href="%s/">%s</a></td><td>%s</td></tr>\n' % (112 -1 escape(repo), escape(repo), escape(config.get(repo, 'desc'))113 -1 ))-1 96 for repo in config.iter_repos(): -1 97 if config.getboolean(repo, 'http'): -1 98 fh.write('<tr><td><a href="%s/">%s</a></td><td>%s</td></tr>\n' % ( -1 99 escape(repo), escape(repo), escape(config.get(repo, 'desc')) -1 100 )) 114 101 fh.write('</tbody>\n</table>\n') 115 102 fh.write('</main>\n</body>\n</html>\n') 116 103 fh.close() 117 104 118 105 119 106 def render_repo(repo):120 -1 if config.is_public(repo):121 -1 target_dir = os.path.join(WWW_DIR, repo)122 -1 os.makedirs(target_dir, exist_ok=True)123 -1 subprocess.check_call(['stagit', get_repo_dir(repo)], cwd=target_dir)-1 107 target_dir = os.path.join(WWW_DIR, repo) -1 108 os.makedirs(target_dir, exist_ok=True) -1 109 subprocess.check_call(['stagit', get_repo_dir(repo)], cwd=target_dir) 124 110 125 111 126 112 def render_all(): @@ -128,8 +114,9 @@ def render_all(): 128 114 path = os.path.join(WWW_DIR, repo) 129 115 if os.path.isdir(path): 130 116 shutil.rmtree(path)131 -1 for repo in config.iter_user_repos('@public'):132 -1 render_repo(repo)-1 117 for repo in config.iter_repos(): -1 118 if config.getboolean(repo, 'http'): -1 119 render_repo(repo) 133 120 render_index() 134 121 135 122
diff --git a/gitolite/update.py b/gitolite/update.py
@@ -1,38 +0,0 @@1 -1 #!/usr/bin/env python32 -1 # see man githooks3 -14 -1 import os5 -1 import sys6 -1 import subprocess7 -18 -1 import stagit9 -110 -111 -1 def get_access(repo, ref, oldrev, newrev):12 -1 # detect tags13 -1 if ref.startswith('refs/tags/'):14 -1 return '+'15 -116 -1 # detect create/delete17 -1 if '0' * 40 in [oldrev, newrev]:18 -1 return '+'19 -120 -1 # detect force push21 -1 mergebase = subprocess.check_output(22 -1 ['git', 'merge-base', oldrev, newrev], cwd=stagit.get_repo_dir(repo),23 -1 )24 -1 if oldrev != mergebase.decode('ascii').rstrip():25 -1 return '+'26 -127 -1 return 'w'28 -129 -130 -1 if __name__ == '__main__':31 -1 user = os.environ['STAGIT_USER']32 -1 repo = os.environ['STAGIT_REPO']33 -1 ref, oldrev, newrev = sys.argv[1:]34 -135 -1 access = get_access(repo, ref, oldrev, newrev)36 -1 if not stagit.config.has_perm(repo, user, access):37 -1 sys.stderr.write('access denied\n')38 -1 sys.exit(1)