From 98cbb8514b0b3394ecb9fa9894fc93c2ceeda3af Mon Sep 17 00:00:00 2001 From: John Kubiatowicz Date: Wed, 23 Nov 2011 16:44:25 -0800 Subject: [PATCH 01/11] Added hooks, policy and rakefile to install selinux policy for this plugin --- .gitignore | 2 + README.mkd | 38 +++ app/models/git_hosting_settings_observer.rb | 23 +- .../settings/_redmine_git_hosting.html.erb | 7 +- config/locales/bg.yml | 1 + config/locales/bs.yml | 1 + config/locales/ca.yml | 1 + config/locales/cs.yml | 1 + config/locales/da.yml | 1 + config/locales/de.yml | 1 + config/locales/el.yml | 1 + config/locales/en.yml | 1 + config/locales/es.yml | 1 + config/locales/fi.yml | 1 + config/locales/fr.yml | 1 + config/locales/gl.yml | 1 + config/locales/he.yml | 1 + config/locales/hu.yml | 1 + config/locales/id.yml | 1 + config/locales/it.yml | 1 + config/locales/ja.yml | 1 + config/locales/ko.yml | 1 + config/locales/lt.yml | 1 + config/locales/nl.yml | 1 + config/locales/no.yml | 1 + config/locales/pl.yml | 1 + config/locales/pt-BR.yml | 1 + config/locales/pt.yml | 1 + config/locales/ro.yml | 1 + config/locales/ru.yml | 1 + config/locales/sk.yml | 1 + config/locales/sl.yml | 1 + config/locales/sr.yml | 1 + config/locales/sv.yml | 1 + config/locales/th.yml | 1 + config/locales/tr.yml | 1 + config/locales/uk.yml | 1 + config/locales/vi.yml | 1 + config/locales/zh-TW.yml | 1 + config/locales/zh.yml | 1 + lib/git_hosting.rb | 62 +++- selinux/README | 67 +++++ selinux/redmine_git.fc | 9 + selinux/redmine_git.if | 131 +++++++++ selinux/redmine_git.pp | Bin 0 -> 127822 bytes selinux/redmine_git.sh | 44 +++ selinux/redmine_git.te | 69 +++++ tasks/selinux.rake | 273 ++++++++++++++++++ 48 files changed, 739 insertions(+), 22 deletions(-) create mode 100644 selinux/README create mode 100644 selinux/redmine_git.fc create mode 100644 selinux/redmine_git.if create mode 100644 selinux/redmine_git.pp create mode 100755 selinux/redmine_git.sh create mode 100644 selinux/redmine_git.te create mode 100644 tasks/selinux.rake diff --git a/.gitignore b/.gitignore index a01ee289f..5412c5a54 100755 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .*.swp +bin/ +selinux/tmp/ diff --git a/README.mkd b/README.mkd index 945b9cf82..dfae03514 100755 --- a/README.mkd +++ b/README.mkd @@ -246,6 +246,44 @@ This library allows you to quickly deploy ChiliProject, with this plugin to an u chili\_test.sh script, modifying the variables in those scripts as desired. This library is still under development, so these instructions may need to be updated in the near future. +## Selinux Configuration for redmine + +This plugin can be configured to run with selinux. We have included a rakefile in tasks/selinux.rake to assist +with installing with selinux. You should start by editing init.rb and migrating as described above. Then, you +can execute one of the selinux rake tasks (from the redmine root). For instance, the simplest option installs +a selinux configuration for both redmine and the redmine_git_hosting plugin: + + rake selinux:install RAILS_ENV=production + +This will generate the redmine_git_hosting binaries in ./bin, install a selinux policy for these binaries (called +redmine_git.pp), then install a complete context for redmine as follows: + +**(1)** Most of redmine will be marked with "public_content_rw_t". + +**(2)** The dispatch files in Rails.root/public/dispatch.* will be marked with "httpd_sys_script_exec_t" + +**(3)** The redmine_git_hosting binaries in Rails.root/vendor/plugins/redmine_git_hosting/bin will be labeled +with "httpd_redmine_git_script_exec_t", which has been crafted to allow the sudo behavior required by these +binaries. + +Note that this rake file has additional options. For instance, you can specify redmine roots with regular +expressions (not globbed expessions!) as follows (notice the use of double quotes): + + rake selinux:install RAILS_ENV=production ROOT_PATTERN="/source/.*/redmine" + +These additional options are documented in the selinux.rake file. + +Once this plugin is placed under selinux control, three of the redmine_git_hosting settings can +no longer be modified from the settings page. They are: 'gitUser', 'gitoliteIdentityFile', and +'gitoliteIdentityPublicKeyFile'. The plugin settings page will make this clear. The simplest way to +modify these options is to temporarily place your system into permissive mode, refresh the setting page, +change options, then place your system back into enforcing mode. Alternatively, you can alter the init.rb +file and reinstall the plugin. Under normal operation, you will get one selinux complaint about /bin/touch +in your log each time that you visit the plugin settings page. + +This rakefile and selinux configuration has been primarily tested on Redhat Enterprise Linux version 6.x +with apache and fcgi. Other configurations may require slight tweaking. + ## Tested Configurations This plugin has been primarily tested on Ubuntu Server 10.10 and 11.04 (32 and 64 bit) with ChiliProject v1.x, diff --git a/app/models/git_hosting_settings_observer.rb b/app/models/git_hosting_settings_observer.rb index fee8d8a3f..fcd16e9ae 100644 --- a/app/models/git_hosting_settings_observer.rb +++ b/app/models/git_hosting_settings_observer.rb @@ -5,6 +5,8 @@ class GitHostingSettingsObserver < ActiveRecord::Observer @@old_hook_asynch = Setting.plugin_redmine_git_hosting['gitHooksAreAsynchronous'] @@old_http_server = Setting.plugin_redmine_git_hosting['httpServer'] @@old_git_user = Setting.plugin_redmine_git_hosting['gitUser'] + @@old_gitolite_identity = Setting.plugin_redmine_git_hosting['gitoliteIdentityFile'] + @@old_gitolite_publickey = Setting.plugin_redmine_git_hosting['gitoliteIdentityPublicKeyFile'] @@old_repo_base = Setting.plugin_redmine_git_hosting['gitRepositoryBasePath'] @@ -14,12 +16,25 @@ def reload_this_observer end end - - + def before_save(object) + if object.name == "plugin_redmine_git_hosting" && !GitHosting.bin_dir_writeable? + # If bin directory not alterable, don't alow changes to + # Git Username, or Gitolite public or private keys + valuehash = object.value + valuehash['gitUser'] = @@old_git_user + valuehash['gitoliteIdentityFile'] = @@old_gitolite_identity + valuehash['gitoliteIdentityPublicKeyFile'] = @@old_gitolite_publickey + object.value = valuehash + end + end + def after_save(object) if object.name == "plugin_redmine_git_hosting" - %x[ rm -rf '#{ GitHosting.get_tmp_dir }' ] + if GitHosting.bin_dir_writeable? + %x[ rm -rf '#{ GitHosting.get_tmp_dir }' ] + %x[ rm -rf '#{ GitHosting.get_bin_dir }' ] + end if @@old_repo_base != object.value['gitRepositoryBasePath'] GitHostingObserver.set_update_active(false) @@ -49,6 +64,8 @@ def after_save(object) @@old_hook_asynch = object.value['gitHooksAreAsynchronous'] @@old_http_server = object.value['httpServer'] @@old_git_user = object.value['gitUser'] + @@old_gitolite_identity = object.value['gitoliteIdentityFile'] + @@old_gitolite_publickey = object.value['gitoliteIdentityPublicKeyFile'] @@old_repo_base = object.value['gitRepositoryBasePath'] end diff --git a/app/views/settings/_redmine_git_hosting.html.erb b/app/views/settings/_redmine_git_hosting.html.erb index c6b1e7760..9e6527c84 100644 --- a/app/views/settings/_redmine_git_hosting.html.erb +++ b/app/views/settings/_redmine_git_hosting.html.erb @@ -26,19 +26,19 @@

- + <%= text_field_tag("settings[gitUser]", @settings['gitUser'].split(/[\r\n\t ,;]+/).join("\n"), :size => 60) %>

- + <%= text_field_tag("settings[gitoliteIdentityFile]", @settings['gitoliteIdentityFile'], :size => 60) %>

- + <%= text_field_tag("settings[gitoliteIdentityPublicKeyFile]", @settings['gitoliteIdentityPublicKeyFile'], :size => 60) %>

@@ -48,7 +48,6 @@

-

<%= l(:label_git_cache_parameters)%>

diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/bs.yml b/config/locales/bs.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/bs.yml +++ b/config/locales/bs.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/da.yml b/config/locales/da.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/de.yml b/config/locales/de.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/el.yml b/config/locales/el.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/en.yml b/config/locales/en.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/es.yml b/config/locales/es.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/he.yml b/config/locales/he.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/id.yml b/config/locales/id.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/it.yml b/config/locales/it.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/no.yml b/config/locales/no.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 90c31bf3a..e9989cd0e 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -7,6 +7,7 @@ pt-BR: label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Ficheiro de identidade SSH Gitolite (Chave Privada) label_git_repository_base_path: 'Directório Git base (Relativo à "home" do utilizador git)' + label_cannot_change_selinux: 'não pode ser mudada (selinux)' field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/pt.yml b/config/locales/pt.yml index dc5f5f1d6..f6db9e33d 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -7,6 +7,7 @@ pt: label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Ficheiro de identidade SSH Gitolite (Chave Privada) label_git_repository_base_path: 'Directório Git base (Relativo à "home" do utilizador git)' + label_cannot_change_selinux: 'não pode ser mudada (selinux)' field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/ro.yml b/config/locales/ro.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/ro.yml +++ b/config/locales/ro.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/sr.yml b/config/locales/sr.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/th.yml b/config/locales/th.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 2ce8a4693..723401ce1 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -7,6 +7,7 @@ label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon field_git_http: Git Smart HTTP diff --git a/lib/git_hosting.rb b/lib/git_hosting.rb index 8bcea3432..07aa7e0e3 100755 --- a/lib/git_hosting.rb +++ b/lib/git_hosting.rb @@ -23,6 +23,10 @@ def self.web_user return @@web_user end + def self.web_user=(setuser) + @@web_user = setuser + end + def self.git_user Setting.plugin_redmine_git_hosting['gitUser'] end @@ -92,7 +96,7 @@ def self.sudo_web_to_git_user @@sudo_web_to_git_user_stamp = Time.new return @@sudo_web_to_git_user_cached end - test = %x[sudo -nu #{git_user} echo "yes"] + test = %x[#{GitHosting.git_user_runner} echo "yes"] if test.match(/yes/) @@sudo_web_to_git_user_cached = true @@sudo_web_to_git_user_stamp = Time.new @@ -146,7 +150,7 @@ def self.add_route_for_project_with_map(p,m) end end def self.get_tmp_dir - @@git_hosting_tmp_dir ||= File.join(Dir.tmpdir, "redmine_git_hosting") + @@git_hosting_tmp_dir ||= File.join(Dir.tmpdir, "redmine_git_hosting", "#{git_user}") if !File.directory?(@@git_hosting_tmp_dir) %x[mkdir -p "#{@@git_hosting_tmp_dir}"] %x[chmod 700 "#{@@git_hosting_tmp_dir}"] @@ -154,17 +158,51 @@ def self.get_tmp_dir end return @@git_hosting_tmp_dir end + def self.get_bin_dir + @@git_hosting_bin_dir ||= + Rails.root.join("vendor/plugins/redmine_git_hosting/bin") + if !File.directory?(@@git_hosting_bin_dir) + logger.error "Creating bin directory: #{@@git_hosting_bin_dir}, Owner #{web_user}" + %x[mkdir -p "#{@@git_hosting_bin_dir}"] + %x[chmod 750 "#{@@git_hosting_bin_dir}"] + %x[chown #{web_user} "#{@@git_hosting_bin_dir}"] + end + if !File.directory?(@@git_hosting_bin_dir) + logger.error "Cannot create bin directory: #{@@git_hosting_bin_dir}" + end + return @@git_hosting_bin_dir + end - + @@git_bin_dir_writeable = nil + def self.bin_dir_writeable?(*option) + @@git_bin_dir_writeable = nil if option.length > 0 && option[0] == :reset + if @@git_bin_dir_writeable == nil + mybindir = get_bin_dir + mytestfile = "#{mybindir}/writecheck" + if (!File.directory?(mybindir)) + @@git_bin_dir_writeable = false + else + %x[touch "#{mytestfile}"] + if (!File.exists?("#{mytestfile}")) + @@git_bin_dir_writeable = false + else + %x[rm "#{mytestfile}"] + @@git_bin_dir_writeable = true + end + end + end + @@git_bin_dir_writeable + end def self.git_exec_path - return File.join(get_tmp_dir(), "run_git_as_git_user") + return File.join(get_bin_dir(), "run_git_as_git_user") end + def self.gitolite_ssh_path - return File.join(get_tmp_dir(), "gitolite_admin_ssh") + return File.join(get_bin_dir(), "gitolite_admin_ssh") end def self.git_user_runner_path - return File.join(get_tmp_dir(), "run_as_git_user") + return File.join(get_bin_dir(), "run_as_git_user") end @@ -189,7 +227,7 @@ def self.git_user_runner def self.update_git_exec - logger.info "Setting up #{get_tmp_dir()}" + logger.info "Setting up #{get_bin_dir()}" gitolite_key=Setting.plugin_redmine_git_hosting['gitoliteIdentityFile'] File.open(gitolite_ssh_path(), "w") do |f| @@ -211,8 +249,6 @@ def self.update_git_exec sudo_version = 100*100*(split_version[0].to_i) + 100*(split_version[1].to_i) + split_version[2].to_i sudo_version_switch = (100*100*1) + (100 * 7) + 3 - - File.open(git_exec_path(), "w") do |f| f.puts '#!/bin/sh' f.puts "if [ \"\$(whoami)\" = \"#{git_user}\" ] ; then" @@ -251,14 +287,10 @@ def self.update_git_exec f.puts '}' end if !File.exists?(git_user_runner_path()) - - File.chmod(0550, git_exec_path()) File.chmod(0550, gitolite_ssh_path()) File.chmod(0550, git_user_runner_path()) - - - + %x[chown #{web_user} -R "#{@@git_hosting_bin_dir}"] end @@ -291,7 +323,7 @@ def self.clone_or_pull_gitolite_admin # clone/pull from admin repo local_dir = get_tmp_dir() if File.exists? "#{local_dir}/gitolite-admin" - logger.info "Fethcing changes for #{local_dir}/gitolite-admin" + logger.info "Fetching changes for #{local_dir}/gitolite-admin" %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' fetch] %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' merge FETCH_HEAD] else diff --git a/selinux/README b/selinux/README new file mode 100644 index 000000000..a877fb20b --- /dev/null +++ b/selinux/README @@ -0,0 +1,67 @@ +This directory contains a selinux policy crafted to cover the sudo and +ssh scripts for the redmine_git_hosting plugin. In it, we define a new +httpd-compatible type, "httpd_redmine_git_script_exec_t" which can be +placed on the redmine_git_hosting/bin directory to allow sudo access +from redmine code. The basic assumption is that scripts placed into +this directory will be called from context "httpd_script_t" (i.e +redmine). + +Once this plugin is placed under selinux control, three of the +redmine_git_hosting settings can no longer be modified from the +settings page. They are: 'gitUser', 'gitoliteIdentityFile', and +'gitoliteIdentityPublicKeyFile'. The plugin settings page will make +this clear. The simplest way to modify these options is to +temporarily place your system into permissive mode, refresh the +setting page, change options, then place your system back into +enforcing mode. Alternatively, you can alter the init.rb file and +reinstall the plugin. Under normal operation, you will get one +selinux complaint about /bin/touch in your log each time that you +visit the plugin settings page. + +******************* INSTALLATION AND SETUP ************************* +Note that the redmine_git_hosting/bin directory must be constructed +statically so that it can be labeled. You can do this with a series +of rake tasks at the top-level of the redmine directory (after fixing +up the defaults in the redmine_git_hosting init.rb file): + + # Build bin directory with customized scripts for redmine_git_hosting, + # install new selinux policy, and install complete selinux context for + # redmine+this plugin: + + rake selinux:install RAILS_ENV=production + +Since redmine doesn't currently have a selinux install option, this +installation command is only available for this plugin. What this +will do is label the whole redmine site with "public_content_rw_t", +with the exception of the "dispatch*" files in public (set to +"httpd_script_exec_t") and the scripts in redmine_git_hosting/bin (set +to "httpd_redmine_git_script_exec_t"). + +If you happen to have multiple redmine installations, you can use a +regular expression to describe the redmine root directories (this will +translate into file context descriptions). For instance, if you have +multiple redmine installations in directories whose paths start with +"/source" and end with "redmine" you can use (notice the use of +double-quotes!): + + rake selinux:install RAILS_ENV=production ROOT_PATTERN="/source/.*/redmine" + +Somewhat less far-reaching options include: + + # Build bin directory with customized scripts for redmine_git_hosting, + # install new selinux policy, and install selinux context for + # the redmine_git_hosting plugin + + rake selinux:redmine_git_hosting:install + +Finally, for those who are hand-crafting their own file context: + + # Build bin directory with customized scripts for redmine_git_hosting + # and install new selinux policy. No file contexts will be + # installed (so that you must do customization afterwards). + + rake selinux:redmine_git_hosting:install_scripts_and_policy + + + + diff --git a/selinux/redmine_git.fc b/selinux/redmine_git.fc new file mode 100644 index 000000000..7924d2496 --- /dev/null +++ b/selinux/redmine_git.fc @@ -0,0 +1,9 @@ +# We do not install contexts as part of the policy, since it is likely +# to be overridden by the local policy for parent directories. The rakefile +# ( ../tasks/selinux.rake) will install file contexts "manually". +# +# If you really want to install a file context, uncomment the following +# and rerun the install script or rake task +# +# /.*/redmine/vendor/plugins/redmine_git_hosting/bin(/.*)? httpd_redmine_git_script_exec_t + diff --git a/selinux/redmine_git.if b/selinux/redmine_git.if new file mode 100644 index 000000000..f5e66e48a --- /dev/null +++ b/selinux/redmine_git.if @@ -0,0 +1,131 @@ + +##

policy for httpd_redmine_git_script + + +######################################## +## +## Execute a domain transition to run httpd_redmine_git_script. +## +## +## +## Domain allowed access. +## +## +# +interface(`httpd_redmine_git_script_domtrans',` + gen_require(` + type httpd_redmine_git_script_t, httpd_redmine_git_script_exec_t; + ') + + domtrans_pattern($1, httpd_redmine_git_script_exec_t, httpd_redmine_git_script_t) +') + + +######################################## +## +## Search httpd_redmine_git_script rw directories. +## +## +## +## Domain allowed access. +## +## +# +interface(`httpd_redmine_git_script_search_rw_dir',` + gen_require(` + type httpd_redmine_git_script_rw_t; + ') + + allow $1 httpd_redmine_git_script_rw_t:dir search_dir_perms; + files_search_rw($1) +') + +######################################## +## +## Read httpd_redmine_git_script rw files. +## +## +## +## Domain allowed access. +## +## +# +interface(`httpd_redmine_git_script_read_rw_files',` + gen_require(` + type httpd_redmine_git_script_rw_t; + ') + + allow $1 httpd_redmine_git_script_rw_t:file r_file_perms; + allow $1 httpd_redmine_git_script_rw_t:dir list_dir_perms; + files_search_rw($1) +') + +######################################## +## +## Create, read, write, and delete +## httpd_redmine_git_script rw files. +## +## +## +## Domain allowed access. +## +## +# +interface(`httpd_redmine_git_script_manage_rw_files',` + gen_require(` + type httpd_redmine_git_script_rw_t; + ') + + manage_files_pattern($1, httpd_redmine_git_script_rw_t, httpd_redmine_git_script_rw_t) +') + +######################################## +## +## Create, read, write, and delete +## httpd_redmine_git_script rw dirs. +## +## +## +## Domain allowed access. +## +## +# +interface(`httpd_redmine_git_script_manage_rw_dirs',` + gen_require(` + type httpd_redmine_git_script_rw_t; + ') + + manage_dirs_pattern($1, httpd_redmine_git_script_rw_t, httpd_redmine_git_script_rw_t) +') + + +######################################## +## +## All of the rules required to administrate +## an httpd_redmine_git_script environment +## +## +## +## Domain allowed access. +## +## +## +## +## Role allowed access. +## +## +## +# +interface(`httpd_redmine_git_script_admin',` + gen_require(` + type httpd_redmine_git_script_t; + type httpd_redmine_git_script_rw_t; + ') + + allow $1 httpd_redmine_git_script_t:process { ptrace signal_perms }; + ps_process_pattern($1, httpd_redmine_git_script_t) + + files_search_etc($1) + admin_pattern($1, httpd_redmine_git_script_rw_t) + +') diff --git a/selinux/redmine_git.pp b/selinux/redmine_git.pp new file mode 100644 index 0000000000000000000000000000000000000000..bfe21e6945a9e61a536b6baf50f119f2006732a9 GIT binary patch literal 127822 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 redmine_git.te + # Fall though and rebuild policy + else + exit 0 + fi + else + echo "No new avcs found" + exit 0 + fi + else + echo -e $USAGE + exit 1 + fi +elif [ $# -ge 2 ] ; then + echo -e $USAGE + exit 1 +fi + +echo "Building and Loading Policy" +set -x +make -f /usr/share/selinux/devel/Makefile +/usr/sbin/semodule -i redmine_git.pp + diff --git a/selinux/redmine_git.te b/selinux/redmine_git.te new file mode 100644 index 000000000..54b0a7aa1 --- /dev/null +++ b/selinux/redmine_git.te @@ -0,0 +1,69 @@ +policy_module(redmine_git,1.0.0) + +######################################## +# +# Declarations +# +require { + type httpd_t, httpd_sys_script_t, httpd_sys_script_exec_t; + type sudo_db_t; + type httpd_redmine_git_script_t; + class process { setrlimit setfscreate }; + class netlink_route_socket { write getattr read bind create nlmsg_read }; + class capability { setuid sys_resource setgid }; + class dir { getattr search write}; +} + +apache_content_template(redmine_git) + +permissive httpd_redmine_git_script_t; + +######################################## +# +# httpd_redmine_git_script local policy +# +######################################## + +manage_dirs_pattern(httpd_redmine_git_script_t, httpd_redmine_git_script_rw_t, httpd_redmine_git_script_rw_t) +manage_files_pattern(httpd_redmine_git_script_t, httpd_redmine_git_script_rw_t, httpd_redmine_git_script_rw_t) + +domain_use_interactive_fds(httpd_redmine_git_script_t) + +files_read_etc_files(httpd_redmine_git_script_t) + +miscfiles_read_localization(httpd_redmine_git_script_t) + +# Allow our scripts to be called by redmine/apache +httpd_redmine_git_script_domtrans(httpd_sys_script_t) + +# Allow us to access to rest of redmine site +miscfiles_read_public_files(httpd_redmine_git_script_t) +miscfiles_manage_public_files(httpd_redmine_git_script_t) + +#============= httpd_redmine_git_script_t ============== +#Specific capabilities identified by audit2allow + +allow httpd_redmine_git_script_t self:capability audit_write; +allow httpd_redmine_git_script_t self:capability { setuid sys_resource setgid }; +allow httpd_redmine_git_script_t self:key write; +allow httpd_redmine_git_script_t self:netlink_audit_socket { write nlmsg_relay create read }; +allow httpd_redmine_git_script_t self:netlink_route_socket { write getattr read bind create nlmsg_read }; +allow httpd_redmine_git_script_t self:process setrlimit; +allow httpd_redmine_git_script_t sudo_db_t:dir { getattr search }; + +gitosis_read_lib_files(httpd_redmine_git_script_t) +gitosis_manage_lib_files(httpd_redmine_git_script_t) + +httpd_rw_stream_sockets(httpd_redmine_git_script_t) +kernel_read_kernel_sysctls(httpd_redmine_git_script_t) +logging_send_syslog_msg(httpd_redmine_git_script_t) + +# These seem to be needed for ssh.... Not sure why ssh needs +# to read and/or validate contexts... +allow httpd_redmine_git_script_t self:process setfscreate; +miscfiles_manage_cert_dirs(httpd_redmine_git_script_t) +miscfiles_manage_cert_files(httpd_redmine_git_script_t) +selinux_load_policy(httpd_redmine_git_script_t) +selinux_validate_context(httpd_redmine_git_script_t) +seutil_read_file_contexts(httpd_redmine_git_script_t) +seutil_search_default_contexts(httpd_redmine_git_script_t) diff --git a/tasks/selinux.rake b/tasks/selinux.rake new file mode 100644 index 000000000..92e1e4c7c --- /dev/null +++ b/tasks/selinux.rake @@ -0,0 +1,273 @@ +################################################################################ +# Rakefile for selinux installation for Redmine+Redmine_Git_Hosting Plugin # +# # +# This rakefile provides a variety of options for configuring the selinux # +# context for Redmine + Redmine_Git_Hosting Plugin. In addition to the usual # +# environment variables (such as RAIL_ENV), this rakefile has one additional # +# variable, ROOT_PATTERN. ROOT_PATTERN holds an optional regular expression # +# (not globbed filename) which describes the possible root locations for # +# redmine installations; note that such patterns must be quoted to avoid # +# attempts by the shell to expand them. If undefined, the rakefile will use # +# the Rails.root for the local installation. # +# # +# TOP-LEVEL TARGETS: # +# # +# These commands should be executed after altering the init.rb file as # +# described in the README.mkb file. Each target type comes in both "install" # +# and "remove" versions. In the following, the environment variables are # +# optional (of course). Default for ROOT_PATTERN is Rails.root # +# # +# 1) Build bin directory with customized scripts for redmine_git_hosting, # +# install new selinux policy, and install complete selinux context for # +# redmine+redmine_git_hosting plugin # +# # +# rake selinux:install RAILS_ENV=xxx ROOT_PATTERN="yyy" # +# rake selinux:remove RAILS_ENV=xxx ROOT_PATTERN="yyy" # +# # +# 2) Build bin directory with customized scripts for redmine_git_hosting, # +# install new selinux policy, and install selinux context for # +# redmine_git_hosting plugin (not for complete redmine installation). This # +# option assumes that the redmine installation (and plugin) code are # +# already labeled as "public_content_rw_t" except for dispatch.* files # +# which should be labeled as "httpd_sys_script_exec_t". # +# # +# rake selinux:redmine_git_hosting:install RAILS_ENV=xxx ROOT_PATTERN="yyy" # +# rake selinux:redmine_git_hosting:remove RAILS_ENV=xxx ROOT_PATTERN="yyy" # +# # +# 3) Build bin directory with customized scripts for redmine_git_hosting and # +# install new selinux policy. Do not install file contexts of any sort. # +# Proper labeling (done in some other way) should have all of redmine # +# (including plugins) labeled as "public_content_rw_t", with the exception # +# of public/dispatch.* (which should be labeled "httpd_sys_script_exec_t") # +# and vendor/plugins/redmine_git_hosting/bin(/.*) which should be labeled # +# with the new label "httpd_redmine_git_script_exec_t". # +# # +# rake selinux:redmine_git_hosting:install_scripts_and_policy RAILS_ENV=xxx ROOT_PATTERN="yyy" +# rake selinux:redmine_git_hosting:remove_scripts_and_policy RAILS_ENV=xxx ROOT_PATTERN="yyy" +# # +################################################################################ + +namespace :selinux do + desc "Configure selinux for Redmine and Redmine_Git_Hosting plugin" + task :install => [:environment,:install_contexts,"selinux:redmine_git_hosting:install"] do + end + + desc "Unconfigure selinux for Redmine and Redmine_Git_Hosting plugin" + task :remove => [:environment,"selinux:redmine_git_hosting:remove",:remove_contexts] do + end + + desc "Install selinux file contexts for redmine (without plugins)" + task :install_contexts => [:environment] do + roots = redmine_roots + root_pattern = redmine_root_pattern + puts "[Installing file contexts for redmine:" + + sh "semanage fcontext -a -t public_content_rw_t \"#{root_pattern}(/.*)?\"" + sh "semanage fcontext -a -t httpd_sys_script_exec_t \"#{root_pattern}/public/dispatch.*\"" + + roots.each do |path| + puts "Setting new context for redmine root instance at #{path}." + sh "restorecon -R -p #{path}" + end + puts "DONE.]" + end + + desc "Remove selinux file contexts for redmine (without plugins)" + task :remove_contexts => [:environment] do + roots = redmine_roots + root_pattern = redmine_root_pattern + puts "[Removing file contexts for redmine (ignoring errors):" + + sh "semanage fcontext -d \"#{root_pattern}(/.*)?\"" + sh "semanage fcontext -d \"#{root_pattern}/public/dispatch.*\"" + + roots.each do |path| + puts "Setting new context for redmine root instance at #{path}." + sh "restorecon -R -p #{path}" + end + puts "DONE.]" + end + + namespace :redmine_git_hosting do + desc "Install scripts, policy, and file context for redmine_git_hosting plugin." + task :install => [:environment,:install_scripts,:install_policy,:install_contexts] do + end + + desc "Remove scripts, policy, and file context for redmine_git_hosting plugin." + task :remove => [:environment,:remove_contexts,:remove_policy,:remove_scripts] do + end + + desc "Install scripts and policy for redmine_git_hosting plugin." + task :install_scripts_and_policy => [:environment,:install_scripts,:install_policy] do + end + + desc "Remove scripts and policy for redmine_git_hosting plugin." + task :remove_scripts_and_policy => [:environment,:remove_policy,:remove_scripts] do + end + + desc "Generate and install redmine_git_hosting shell scripts." + task :install_scripts => [:environment] do + puts "[Generating and installing redmine_git_hosting shell scripts:" + + plugin_roots = redmine_roots("vendor/plugins/redmine_git_hosting") + plugin_roots.each do |path| + if path != "#{Rails.root}/vendor/plugins/redmine_git_hosting" + # Have to call another rails environment. Keep default root in that environment + chdir File.expand_path("#{path}/../../..") do + print %x[rake selinux:redmine_git_hosting:install_scripts_helper] + end + else + Rake::Task["selinux:redmine_git_hosting:install_scripts_helper"].invoke + end + end + puts "DONE.]" + end + + desc "Helper function for generating and installing redmine_git_hosting shell scripts." + task :install_scripts_helper => [:environment] do + web_program = ENV['HTTPD'] || 'httpd' + web_user = ENV['WEB_USER'] || %x[ps aux | grep #{web_program} | sed "s/ .*$//" | sort -u | grep -v `whoami`].split("\n")[0] + GitHosting.web_user = web_user + + # Helper only executed in local environment + path = "#{Rails.root}/vendor/plugins/redmine_git_hosting" + print "Clearing out #{path}/bin directory..." + %x[rm -rf "#{path}/bin"] + puts "Success!" + print "Writing customized scripts to #{path}/bin directory..." + GitHosting.update_git_exec + puts "Success!" + end + + desc "Remove redmine_git_hosting shell scripts." + task :remove_scripts => [:environment] do + puts "[Deleting redmine_git_hosting shell scripts:" + plugin_roots = redmine_roots("vendor/plugins/redmine_git_hosting") + plugin_roots.each do |path| + sh "rm -rf #{path}/bin" + puts "Success!" + end + puts "DONE.]" + end + + desc "Install selinux tags and policy for redmine_git_hosting." + task :install_policy => [:environment] do + puts "[Installing selinux tags and policy for redmine_git_hosting:" + plugin_roots = redmine_roots("vendor/plugins/redmine_git_hosting") + sh "#{plugin_roots[0]}/selinux/redmine_git.sh" + puts "DONE.]" + end + + desc "Remove selinux tags and policy for redmine_git_hosting." + task :remove_policy => [:environment] do + puts "[Deleting selinux tags and policy for redmine_git_hosting." + sh "semodule -r redmine_git | true" + puts "DONE.]" + end + + desc "Install file contexts for redmine_git_hosting plugin." + task :install_contexts => [:environment] do + plugin_roots = redmine_roots("vendor/plugins/redmine_git_hosting") + plugin_root_pattern = redmine_root_pattern("vendor/plugins/redmine_git_hosting") + puts "[Installing file context for redmine_git_hosting plugin:" + sh "semanage fcontext -a -t httpd_redmine_git_script_exec_t \"#{plugin_root_pattern}/bin(/.*)?\" | true" + + plugin_roots.each do |path| + puts "Setting new context for plugin instance at #{path}." + sh "restorecon -R -p #{path}" + end + puts "DONE.]" + end + + desc "Remove file contexts for redmine_git_hosting plugin." + task :remove_contexts => [:environment] do + plugin_roots = redmine_roots("vendor/plugins/redmine_git_hosting") + plugin_root_pattern = redmine_root_pattern("vendor/plugins/redmine_git_hosting") + puts "[Deleting file context for redmine_git_hosting plugin (ignoring errors)." + sh "semanage fcontext -d \"#{plugin_root_pattern}/bin(/.*)?\" | true" + plugin_roots.each do |path| + puts "Setting new context for plugin instance at #{path}." + sh "restorecon -R -p #{path}" + end + puts "DONE.]" + end + end +end + +############################################################################# +# # +# Path support logic # +# # +############################################################################# +@@redmine_roots = {} +@@redmine_root_pattern = ENV['ROOT_PATTERN'] || Rails.root +@@find_maxdepth = 6 + +# Turn a regex file descriptor (file context) into a +# conservative (enclosing) globbed expression +# +# Grabbed this from /usr/sbin/fixfiles... +def regex_to_glob(in_regex) + # clobber anything after space char + my_regex = in_regex.gsub(/\s.*/,"") + my_regex = my_regex.gsub(%r|\(([/\w]+)\)\?|,"{\1,}") + my_regex = my_regex.gsub(%r|([/\w])\?|,"{\1,}") + my_regex = my_regex.gsub(%r|\?.*|,"*") + my_regex = my_regex.gsub(%r|\(.*|,"*") + my_regex = my_regex.gsub(%r|\[.*|,"*") + my_regex = my_regex.gsub(%r|\.\*.*|,"*") + my_regex = my_regex.gsub(%r|\.\+.*|,"*") + + return my_regex +end + +# Return a pattern for the root a redmine installation (as defined +# either by Rails.root or ROOT_PATTERN. +# +# Optional arguments are joined together with "/" as a path and +# appended to the end of the root pattern as described above +def redmine_root_pattern(*optionpath) + if optionpath.length > 0 + pathend = optionpath.join("/") + "#{@@redmine_root_pattern}/#{pathend}" + else + "#{@@redmine_root_pattern}" + end +end + +# Return an array of pointers to redmine directories as defined by +# the redmine_root_pattern (see above). +# +# When optional arguments are included, they are joined together with +# "/" and appended to the end of each redmine root path. +# +# Note that we use the @@redmine_roots hash to cache our results and +# thus avoid repeating work. +def redmine_roots(*optionpath) + if @@redmine_roots["/"].nil? + glob_pattern = regex_to_glob(redmine_root_pattern) + search_command = "find #{glob_pattern} -maxdepth #{@@find_maxdepth} -type d -regextype posix-extended -regex #{@@redmine_root_pattern} -prune" + if glob_pattern =~ /.*[\(\[\*\+\?].*/ + puts "Searching for directories matching \"#{@@redmine_root_pattern}\" (may take a bit):" + puts "#{search_command}" + end + new_roots=%x[#{search_command}].split("\n") + if new_roots.length == 0 + fail "Error: ROOT_PATTERN does not match any directories!" + end + @@redmine_roots["/"] = new_roots + end + if optionpath.length > 0 + pathend = optionpath.join("/") + if @@redmine_roots[pathend].nil? + subpaths = @@redmine_roots["/"].map{|myroot|"#{myroot}/#{pathend}"}.select{|dir|File.directory?(dir)} + if subpaths.length == 0 + fail "Error: ROOT_PATTERN/#{pathend} does not match any directories!" + end + @@redmine_roots[pathend] = subpaths + end + @@redmine_roots[pathend] + else + @@redmine_roots["/"] + end +end From 33aafdd2f9557d31346ddd6856c3b65353f00ab4 Mon Sep 17 00:00:00 2001 From: John Kubiatowicz Date: Wed, 23 Nov 2011 16:46:00 -0800 Subject: [PATCH 02/11] Performance fix for redmine_git_hosting. This prevents a rediculous number of calls to update_repositories caused by the fact that redmine saves to the repository model for every commit that it examines. This patch also prevents multiple calls to update_repositories when adding groups to the membership of a project. You need to migrate plugins to take advantage of new indices: rake db:migrate_plugins --- app/models/git_hosting_observer.rb | 18 +++++-- ...0948_add_indexes_to_gitolite_public_key.rb | 11 +++++ init.rb | 8 ++++ .../patches/git_repository_patch.rb | 11 +++++ .../patches/members_controller_patch.rb | 48 +++++++++++++++++++ lib/git_hosting/patches/repository_patch.rb | 13 ++++- .../patches/sys_controller_patch.rb | 26 ++++++++++ 7 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20111119170948_add_indexes_to_gitolite_public_key.rb create mode 100644 lib/git_hosting/patches/members_controller_patch.rb create mode 100644 lib/git_hosting/patches/sys_controller_patch.rb diff --git a/app/models/git_hosting_observer.rb b/app/models/git_hosting_observer.rb index 8b8de5462..f1db69976 100644 --- a/app/models/git_hosting_observer.rb +++ b/app/models/git_hosting_observer.rb @@ -2,6 +2,7 @@ class GitHostingObserver < ActiveRecord::Observer observe :project, :user, :gitolite_public_key, :member, :role, :repository @@updating_active = true + @@updating_active_stack = 0 @@cached_project_updates = [] def reload_this_observer @@ -12,14 +13,25 @@ def reload_this_observer def self.set_update_active(is_active) - @@updating_active = is_active - if is_active + if !is_active + @@updating_active_stack += 1 + else + @@updating_active_stack -= 1 + if @@updating_active_stack < 0 + @@updating_active_stack = 0 + end + end + + if is_active && @@updating_active_stack == 0 if @@cached_project_updates.length > 0 @@cached_project_updates = @@cached_project_updates.flatten.uniq.compact GitHosting::update_repositories(@@cached_project_updates, false) + @@cached_project_updates = [] end + @@updating_active = true + else + @@updating_active = false end - @@cached_project_updates = [] end diff --git a/db/migrate/20111119170948_add_indexes_to_gitolite_public_key.rb b/db/migrate/20111119170948_add_indexes_to_gitolite_public_key.rb new file mode 100644 index 000000000..d5596275e --- /dev/null +++ b/db/migrate/20111119170948_add_indexes_to_gitolite_public_key.rb @@ -0,0 +1,11 @@ +class AddIndexesToGitolitePublicKey < ActiveRecord::Migration + def self.up + add_index :gitolite_public_keys, :user_id + add_index :gitolite_public_keys, :identifier + end + + def self.down + remove_index :gitolite_public_keys, :user_id + remove_index :gitolite_public_keys, :identifier + end +end diff --git a/init.rb b/init.rb index 5567c40f2..f0337d979 100755 --- a/init.rb +++ b/init.rb @@ -66,6 +66,14 @@ require 'git_hosting/patches/git_repository_patch' Repository::Git.send(:include, GitHosting::Patches::GitRepositoryPatch) + require_dependency 'sys_controller' + require 'git_hosting/patches/sys_controller_patch' + SysController.send(:include, GitHosting::Patches::SysControllerPatch) + + require_dependency 'members_controller' + require 'git_hosting/patches/members_controller_patch' + MembersController.send(:include, GitHosting::Patches::MembersControllerPatch) + require_dependency 'git_hosting/patches/repository_cia_filters' end diff --git a/lib/git_hosting/patches/git_repository_patch.rb b/lib/git_hosting/patches/git_repository_patch.rb index fb9cf2ead..af081f1ff 100644 --- a/lib/git_hosting/patches/git_repository_patch.rb +++ b/lib/git_hosting/patches/git_repository_patch.rb @@ -9,6 +9,16 @@ def extra_report_last_commit_with_always_true true end + def fetch_changesets_with_disable_update + # Turn of updates during repository update + GitHostingObserver.set_update_active(false); + + # Do actual update + fetch_changesets_without_disable_update + + # Reenable updates to perform a single update + GitHostingObserver.set_update_active(true); + end def self.included(base) base.class_eval do @@ -17,6 +27,7 @@ def self.included(base) begin base.send(:alias_method_chain, :report_last_commit, :always_true) base.send(:alias_method_chain, :extra_report_last_commit, :always_true) + base.send(:alias_method_chain, :fetch_changesets, :disable_update) rescue end diff --git a/lib/git_hosting/patches/members_controller_patch.rb b/lib/git_hosting/patches/members_controller_patch.rb new file mode 100644 index 000000000..af9d7d4cb --- /dev/null +++ b/lib/git_hosting/patches/members_controller_patch.rb @@ -0,0 +1,48 @@ +module GitHosting + module Patches + module MembersControllerPatch + def new_with_disable_update + # Turn of updates during repository update + GitHostingObserver.set_update_active(false); + + # Do actual update + new_without_disable_update + + # Reenable updates to perform a single update + GitHostingObserver.set_update_active(true); + end + def edit_with_disable_update + # Turn of updates during repository update + GitHostingObserver.set_update_active(false); + + # Do actual update + edit_without_disable_update + + # Reenable updates to perform a single update + GitHostingObserver.set_update_active(true); + end + def destroy_with_disable_update + # Turn of updates during repository update + GitHostingObserver.set_update_active(false); + + # Do actual update + destroy_without_disable_update + + # Reenable updates to perform a single update + GitHostingObserver.set_update_active(true); + end + + def self.included(base) + base.class_eval do + unloadable + end + begin + base.send(:alias_method_chain, :new, :disable_update) + base.send(:alias_method_chain, :edit, :disable_update) + base.send(:alias_method_chain, :destroy, :disable_update) + rescue + end + end + end + end +end diff --git a/lib/git_hosting/patches/repository_patch.rb b/lib/git_hosting/patches/repository_patch.rb index 134915ca6..6c3695250 100644 --- a/lib/git_hosting/patches/repository_patch.rb +++ b/lib/git_hosting/patches/repository_patch.rb @@ -25,8 +25,17 @@ def factory_with_git_extra_init(klass_name, *args) end return new_repo end - end + def fetch_changesets_with_disable_update + # Turn of updates during repository update + GitHostingObserver.set_update_active(false); + + # Do actual update + fetch_changesets_without_disable_update + # Reenable updates to perform a single update + GitHostingObserver.set_update_active(true); + end + end def self.included(base) base.extend(ClassMethods) @@ -34,9 +43,9 @@ def self.included(base) unloadable class << self alias_method_chain :factory, :git_extra_init + alias_method_chain :fetch_changesets, :disable_update end end - end end end diff --git a/lib/git_hosting/patches/sys_controller_patch.rb b/lib/git_hosting/patches/sys_controller_patch.rb new file mode 100644 index 000000000..6b35c86e5 --- /dev/null +++ b/lib/git_hosting/patches/sys_controller_patch.rb @@ -0,0 +1,26 @@ +module GitHosting + module Patches + module SysControllerPatch + def fetch_changesets_with_disable_update + # Turn of updates during repository update + GitHostingObserver.set_update_active(false); + + # Do actual update + fetch_changesets_without_disable_update + + # Reenable updates to perform a single update + GitHostingObserver.set_update_active(true); + end + + def self.included(base) + base.class_eval do + unloadable + end + begin + base.send(:alias_method_chain, :fetch_changesets, :disable_update) + rescue + end + end + end + end +end From 10bfd7dfeeb66c1681ec454a8e4c2c80914831a4 Mon Sep 17 00:00:00 2001 From: John Kubiatowicz Date: Sat, 26 Nov 2011 11:07:45 -0800 Subject: [PATCH 03/11] More performance improvements. This patch should remove all remaining need for recursive calls to update_repositories. Will log such calls as errors in upcoming major code fix... --- .../gitolite_public_keys_controller.rb | 4 ++ init.rb | 8 +++ .../patches/roles_controller_patch.rb | 36 +++++++++++ .../patches/users_controller_patch.rb | 59 +++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 lib/git_hosting/patches/roles_controller_patch.rb create mode 100644 lib/git_hosting/patches/users_controller_patch.rb diff --git a/app/controllers/gitolite_public_keys_controller.rb b/app/controllers/gitolite_public_keys_controller.rb index e11721d2b..1276f8471 100644 --- a/app/controllers/gitolite_public_keys_controller.rb +++ b/app/controllers/gitolite_public_keys_controller.rb @@ -10,9 +10,11 @@ def edit end def delete + GitHostingObserver.set_update_active(false) @gitolite_public_key[:active] = 0 @gitolite_public_key.save redirect_to url_for(:controller => 'my', :action => 'account') + GitHostingObserver.set_update_active(true) end def update @@ -25,6 +27,7 @@ def update end def create + GitHostingObserver.set_update_active(false) @gitolite_public_key = GitolitePublicKey.new(params[:public_key].merge(:user => @user)) if @gitolite_public_key.save flash[:notice] = l(:notice_public_key_added) @@ -32,6 +35,7 @@ def create @gitolite_public_key = GitolitePublicKey.new(:user => @user) end redirect_to url_for(:controller => 'my', :action => 'account') + GitHostingObserver.set_update_active(true) end protected diff --git a/init.rb b/init.rb index f0337d979..721cdbaca 100755 --- a/init.rb +++ b/init.rb @@ -74,6 +74,14 @@ require 'git_hosting/patches/members_controller_patch' MembersController.send(:include, GitHosting::Patches::MembersControllerPatch) + require_dependency 'users_controller' + require 'git_hosting/patches/users_controller_patch' + UsersController.send(:include, GitHosting::Patches::UsersControllerPatch) + + require_dependency 'roles_controller' + require 'git_hosting/patches/roles_controller_patch' + RolesController.send(:include, GitHosting::Patches::RolesControllerPatch) + require_dependency 'git_hosting/patches/repository_cia_filters' end diff --git a/lib/git_hosting/patches/roles_controller_patch.rb b/lib/git_hosting/patches/roles_controller_patch.rb new file mode 100644 index 000000000..33782df66 --- /dev/null +++ b/lib/git_hosting/patches/roles_controller_patch.rb @@ -0,0 +1,36 @@ +module GitHosting + module Patches + module RolesControllerPatch + def edit_with_disable_update + # Turn of updates during repository update + GitHostingObserver.set_update_active(false); + + # Do actual update + edit_without_disable_update + + # Reenable updates to perform a single update + GitHostingObserver.set_update_active(true); + end + def destroy_with_disable_update + # Turn of updates during repository update + GitHostingObserver.set_update_active(false); + + # Do actual update + destroy_without_disable_update + + # Reenable updates to perform a single update + GitHostingObserver.set_update_active(true); + end + def self.included(base) + base.class_eval do + unloadable + end + begin + base.send(:alias_method_chain, :edit, :disable_update) + base.send(:alias_method_chain, :destroy, :disable_update) + rescue + end + end + end + end +end diff --git a/lib/git_hosting/patches/users_controller_patch.rb b/lib/git_hosting/patches/users_controller_patch.rb new file mode 100644 index 000000000..e5f31f6c4 --- /dev/null +++ b/lib/git_hosting/patches/users_controller_patch.rb @@ -0,0 +1,59 @@ +module GitHosting + module Patches + module UsersControllerPatch + def create_with_disable_update + # Turn of updates during repository update + GitHostingObserver.set_update_active(false); + + # Do actual update + create_without_disable_update + + # Reenable updates to perform a single update + GitHostingObserver.set_update_active(true); + end + def update_with_disable_update + # Turn of updates during repository update + GitHostingObserver.set_update_active(false); + + # Do actual update + update_without_disable_update + + # Reenable updates to perform a single update + GitHostingObserver.set_update_active(true); + end + def destroy_with_disable_update + # Turn of updates during repository update + GitHostingObserver.set_update_active(false); + + # Do actual update + destroy_without_disable_update + + # Reenable updates to perform a single update + GitHostingObserver.set_update_active(true); + end + def edit_membership_with_disable_update + # Turn of updates during repository update + GitHostingObserver.set_update_active(false); + + # Do actual update + edit_membership_without_disable_update + + # Reenable updates to perform a single update + GitHostingObserver.set_update_active(true); + end + + def self.included(base) + base.class_eval do + unloadable + end + begin + base.send(:alias_method_chain, :create, :disable_update) + base.send(:alias_method_chain, :update, :disable_update) + base.send(:alias_method_chain, :destroy, :disable_update) + base.send(:alias_method_chain, :edit_membershipt, :disable_update) + rescue + end + end + end + end +end From db30d7eebf941804504d5d7b6797da12cf2ec070 Mon Sep 17 00:00:00 2001 From: John Kubiatowicz Date: Sat, 26 Nov 2011 22:20:00 -0800 Subject: [PATCH 04/11] Rewrite of primary gitolite interface for better resilience. The primary "update_repositories" function is now largely self-correcting against a variety of errors seen in the field. Also added a bunch of error checking on functions called-out to the shell. Among other things, this code attempts to reconnect the gitolite-admin public key when it gets deleted (can happen). Regular use of 'sys/fetch_changesets' will recorrect any errors in the public key directory and gitolite.conf file. For example, orphan keys that happen to be left behind in the public key directory will be removed, missing keys will be added, etc. One new piece of functionality is the 'recycle_bin' which is used to store deleted repositories immediately after they are deleted (allowing them to be recovered for up to 'gitRecycleExpireTime' hours after deletion). You need to migrate plugins because there are new plugin settings: rake db:migrate_plugins --- README.mkd | 72 ++- app/models/git_hosting_observer.rb | 21 +- app/models/git_hosting_settings_observer.rb | 94 ++- app/models/gitolite_public_key.rb | 10 + .../settings/_redmine_git_hosting.html.erb | 19 + config/locales/bg.yml | 5 +- config/locales/bs.yml | 5 +- config/locales/ca.yml | 5 +- config/locales/cs.yml | 5 +- config/locales/da.yml | 5 +- config/locales/de.yml | 5 +- config/locales/el.yml | 5 +- config/locales/en.yml | 5 +- config/locales/es.yml | 5 +- config/locales/fi.yml | 5 +- config/locales/fr.yml | 5 +- config/locales/gl.yml | 5 +- config/locales/he.yml | 5 +- config/locales/hu.yml | 5 +- config/locales/id.yml | 5 +- config/locales/it.yml | 5 +- config/locales/ja.yml | 5 +- config/locales/ko.yml | 5 +- config/locales/lt.yml | 5 +- config/locales/nl.yml | 5 +- config/locales/no.yml | 5 +- config/locales/pl.yml | 5 +- config/locales/pt-BR.yml | 3 + config/locales/pt.yml | 3 + config/locales/ro.yml | 5 +- config/locales/ru.yml | 5 +- config/locales/sk.yml | 5 +- config/locales/sl.yml | 5 +- config/locales/sr.yml | 5 +- config/locales/sv.yml | 5 +- config/locales/th.yml | 5 +- config/locales/tr.yml | 5 +- config/locales/uk.yml | 5 +- config/locales/vi.yml | 5 +- config/locales/zh-TW.yml | 5 +- config/locales/zh.yml | 5 +- .../20111123214911_add_settings_to_plugin.rb | 34 + init.rb | 10 +- lib/git_hosting.rb | 593 +++++++++++++----- .../patches/repositories_controller_patch.rb | 2 +- lib/git_hosting/patches/repository_patch.rb | 4 +- .../patches/sys_controller_patch.rb | 4 +- lib/gitolite_conf.rb | 50 +- lib/gitolite_recycle.rb | 112 ++++ tasks/gitolite.rake | 25 +- 50 files changed, 967 insertions(+), 259 deletions(-) create mode 100644 db/migrate/20111123214911_add_settings_to_plugin.rb create mode 100755 lib/gitolite_recycle.rb diff --git a/README.mkd b/README.mkd index dfae03514..d9fe779be 100755 --- a/README.mkd +++ b/README.mkd @@ -1,9 +1,9 @@ -# Redmine Git Hosting Plugin (v0.4.2) +# Redmine Git Hosting Plugin (v0.4.2x) A ChiliProject / Redmine plugin which makes configuring your own git hosting easy. This plugin allows straightforward management of gitolite and associated public keys, the git daemon, and integrates code from Scott Schacon's "grack" utility to provide Git Smart HTTP access. Git repositories are automatically created when the repository is created in -redmine. There is also an option to automatically create a git repository for a project, when the project is created. +Redmine. There is also an option to automatically create a git repository for a project, when the project is created. Caching functionality is also implemented to speed page-load times for viewing git repositories. @@ -41,6 +41,9 @@ Note that this guide refers to the "web server user" as the user under which Rai not always) the same as the user that runs the main web server. If you are running Rails under a different user, follow these instructions using that user, not the one for the main web server. +As of the most recent set of patches, this plugin is compatible with running multiple Redmine installations on the same server, each +with the same *or* different gitolite users/repositories. The later configuration (multiple Redmine installations, each with a different +gitolite installation) is particularly useful for a web-hosting scenario with independent developers. ## Step-By-Step configuration instructions @@ -101,12 +104,12 @@ variables are set correctly. To adjust these variables, open an editor and edit Starting on line 22 you will see the settings definitions you should edit. -The *httpServer* variable should be set to the url used to access your redmine site, e.g. www.my-own-personal-git-host-server.com. Note that if Redmine is not -installed in the site root this should include the path to your redmine root, e.g. www.my-own-personal-git-host-server.com/path/to/redmine +The *httpServer* variable should be set to the url used to access your Redmine site, e.g. www.my-own-personal-git-host-server.com. Note that if Redmine is not +installed in the site root this should include the path to your Redmine root, e.g. www.my-own-personal-git-host-server.com/path/to/redmine The *gitServer* will usually be the same as the the httpServer variable -- this is the server name to use to access the gitolite repositories via ssh. This should be -the hostname only, so this will be different from *httpServer* if Redmine is not installed in the site root. In other words, even if redmine is installed in +the hostname only, so this will be different from *httpServer* if Redmine is not installed in the site root. In other words, even if Redmine is installed in www.my-own-personal-git-host-server.com/path/to/redmine, *gitServer* should be set to www.my-own-personal-git-host-server.com @@ -146,12 +149,51 @@ you have both git and svn (or hg, or cvs etc.) repositories, this may cause prob *Delete Git Repository When Project Is Deleted* can be enabled to let this plugin control repository deletion as well as repository creation. By default, this feature is disabled and when a repository is deleted in ChiliProject / Redmine, it is not deleted in gitolite. This is a safety feature to prevent -the accidental loss of data. If this feature is enabled, the safety is turned off and the repository files will be permanently deleted when the Project/Repository is deleted in ChiliProject/Redmine. +the accidental loss of data. If this feature is enabled, the safety is turned off and the repository files will be deleted when the Project/Repository is +deleted in ChiliProject/Redmine. Note, however, that even when this feature is enabled, deleted repositories are placed into a "recycle_bin" for a configurable +amount of time (defaulting to 24 hours) and can be recovered by recreating the project in Redmine with the same Identifier. Details are placed in the log. + +The *gitRecycleBasePath* is the path *relative to the git user root* where deleted repositories are placed. This path should end in a path separator, e.g. '/'. +Deleted repositories are kept here for up to *gitRecycleExpireTime* hours (configurable, defaults to 24.0 hours). + +The *gitLockWaitTime* represents the amount of time that the plugin will wait in attempting to acquire its internal synchronization lock before giving up. +You probably will not need to change this value. *Show Checkout URLs* can be disabled to hide the git URL bar in the repository tab. It is enabled by default. See below in the "Caching" section of this readme for more information on caching and how the caching variables should be configured. +## Resychronization of gitolite configuration + +Whenever a Redmine `fetch_changesets()` operation is executed (i.e. `http://REDMINE_ROOT/sys/fetch_changesets?key=xxx`), this plugin will check the +gitolite keydir and configuration file for consistency. It will correct any errors that it finds. Further, regular execution of a fetch_changesets operation +will make sure that repositories placed in the recycle_bin (during delete operations) will be expired and removed. Since there still seem to be some +phantom synchronization problems, it is recommended that you execute `fetch_changesets()` regularly (every 15 to 30 minutes). + +Two rake tasks can additionally be used for resynchronization (although these are redundant with executing `fetch_changesets()` through other means). + +**(1)** To fixup the gitolite configuration file, fix errors, and delete expired files in the recycle_bin, execute: + + RAILS_ENV=production rake gitolite:update_repositories + +**(2)** To perform all the above operations while at the same time fetching changesets for all repositories, execute: + + RAILS_ENV=production rake gitolite:fetch_changes + +**Note that it is very important that these commands be run as *www-user* (or whatever your web server user happens to be), lest you get permission problems later.** +(The same is true of any `fetch_changesets()` operation initiated without using the web server, i.e. through the command line or from the cron daemon). + +## Interaction with non-Redmine gitolite users + +This plugin respects gitolite repositories that are managed outside of Redmine or managed by both Redmine and non-Redmine users: + +* When performing a *fetch_changesets()* operation, this plugin will delete and reestablish all keys that are of the form "redmine_", + since it considers these to be under its exclusive control. A special token, called "redmine_dummy_key", is used as a placeholder when no access + is granted for a given repository. +* Keys other than "redmine_" are left untouched and can be in projects by themselves or mixed in with projects managed by redmine. +* When a Redmine-managed project is deleted (with the *Delete Git Repository When Project Is Deleted* option enabled), its corresponding git repository + *will not be deleted/recycled* if there are non-Redmine keys in the gitolite.conf file. + ## A Note About PATH variables One major source of issues with this plugin is that Rails needs to be able to run both *sudo* and *git*. Specifically, these programs need to be in one of the directories specified by @@ -246,19 +288,19 @@ This library allows you to quickly deploy ChiliProject, with this plugin to an u chili\_test.sh script, modifying the variables in those scripts as desired. This library is still under development, so these instructions may need to be updated in the near future. -## Selinux Configuration for redmine +## Selinux Configuration for Redmine This plugin can be configured to run with selinux. We have included a rakefile in tasks/selinux.rake to assist with installing with selinux. You should start by editing init.rb and migrating as described above. Then, you -can execute one of the selinux rake tasks (from the redmine root). For instance, the simplest option installs -a selinux configuration for both redmine and the redmine_git_hosting plugin: +can execute one of the selinux rake tasks (from the Redmine root). For instance, the simplest option installs +a selinux configuration for both Redmine and the redmine_git_hosting plugin: rake selinux:install RAILS_ENV=production This will generate the redmine_git_hosting binaries in ./bin, install a selinux policy for these binaries (called -redmine_git.pp), then install a complete context for redmine as follows: +redmine_git.pp), then install a complete context for Redmine as follows: -**(1)** Most of redmine will be marked with "public_content_rw_t". +**(1)** Most of Redmine will be marked with "public_content_rw_t". **(2)** The dispatch files in Rails.root/public/dispatch.* will be marked with "httpd_sys_script_exec_t" @@ -266,7 +308,7 @@ redmine_git.pp), then install a complete context for redmine as follows: with "httpd_redmine_git_script_exec_t", which has been crafted to allow the sudo behavior required by these binaries. -Note that this rake file has additional options. For instance, you can specify redmine roots with regular +Note that this rake file has additional options. For instance, you can specify Redmine roots with regular expressions (not globbed expessions!) as follows (notice the use of double quotes): rake selinux:install RAILS_ENV=production ROOT_PATTERN="/source/.*/redmine" @@ -288,7 +330,8 @@ with apache and fcgi. Other configurations may require slight tweaking. This plugin has been primarily tested on Ubuntu Server 10.10 and 11.04 (32 and 64 bit) with ChiliProject v1.x, ChiliProject 2.0.0 and Redmine 1.2.1 with PostgreSQL as the database (July, 2011). It is possible that some -debugging will be necessary for other configurations. +debugging will be necessary for other configurations. Selinux configurations were tested under Redhat Enterprise Linux +version 6.x with apache and fcgi. ## Required gems @@ -308,7 +351,8 @@ This plugin is based largely on the Gitosis plugin by Jan Schulz-Hofen for http: were provided by github users untoldwind, tingar and ericpaulbishop. These updates were merged together and expanded upon by Eric Bishop to create this more comprehensive Git Hosting plugin. +Copyright (c) 2011 John Kubiatowicz (kubitron@cs.berkeley.edu) MIT License. + Copyright (c) 2010-2011 Eric Bishop (ericpaulbishop@gmail.com) MIT License. Copyright (c) 2009-2010 Jan Schulz-Hofen, ROCKET RENTALS GmbH (http://www.rocket-rentals.de). MIT License. - diff --git a/app/models/git_hosting_observer.rb b/app/models/git_hosting_observer.rb index f1db69976..9599a0b7c 100644 --- a/app/models/git_hosting_observer.rb +++ b/app/models/git_hosting_observer.rb @@ -3,6 +3,7 @@ class GitHostingObserver < ActiveRecord::Observer @@updating_active = true @@updating_active_stack = 0 + @@updating_active_flags = {} @@cached_project_updates = [] def reload_this_observer @@ -13,6 +14,12 @@ def reload_this_observer def self.set_update_active(is_active) + case is_active + when Symbol then @@updating_active_flags[is_active] = true + when Hash then @updating_active_flags.merge(is_active) + when Project then @@cached_project_updates << is_active + end + if !is_active @@updating_active_stack += 1 else @@ -23,10 +30,11 @@ def self.set_update_active(is_active) end if is_active && @@updating_active_stack == 0 - if @@cached_project_updates.length > 0 + if @@cached_project_updates.length > 0 || !@@updating_active_flags.empty? @@cached_project_updates = @@cached_project_updates.flatten.uniq.compact - GitHosting::update_repositories(@@cached_project_updates, false) + GitHosting::update_repositories(@@cached_project_updates, @@updating_active_flags) @@cached_project_updates = [] + @@updating_active_flags = {} end @@updating_active = true else @@ -34,11 +42,10 @@ def self.set_update_active(is_active) end end - def before_destroy(object) if object.is_a?(Repository::Git) if Setting.plugin_redmine_git_hosting['deleteGitRepositories'] == "true" - GitHosting::update_repositories(object.project, true) + GitHosting::delete_repository(object.project) %x[#{GitHosting::git_user_runner} 'rm -rf #{object.url}' ] end GitHosting::clear_cache_for_project(object.project) @@ -63,7 +70,7 @@ def before_save(object) def after_save(object) update_repositories(object) end - + def after_destroy(object) if !object.is_a?(Repository::Git) @@ -74,9 +81,7 @@ def after_destroy(object) protected - def update_repositories(object) - projects = [] case object when Repository::Git then projects.push(object.project) @@ -87,7 +92,7 @@ def update_repositories(object) end if(projects.length > 0) if (@@updating_active) - GitHosting::update_repositories(projects, false) + GitHosting::update_repositories(projects) else @@cached_project_updates.concat(projects) end diff --git a/app/models/git_hosting_settings_observer.rb b/app/models/git_hosting_settings_observer.rb index fcd16e9ae..884779cb6 100644 --- a/app/models/git_hosting_settings_observer.rb +++ b/app/models/git_hosting_settings_observer.rb @@ -1,14 +1,7 @@ class GitHostingSettingsObserver < ActiveRecord::Observer observe :setting - @@old_hook_debug = Setting.plugin_redmine_git_hosting['gitHooksDebug'] - @@old_hook_asynch = Setting.plugin_redmine_git_hosting['gitHooksAreAsynchronous'] - @@old_http_server = Setting.plugin_redmine_git_hosting['httpServer'] - @@old_git_user = Setting.plugin_redmine_git_hosting['gitUser'] - @@old_gitolite_identity = Setting.plugin_redmine_git_hosting['gitoliteIdentityFile'] - @@old_gitolite_publickey = Setting.plugin_redmine_git_hosting['gitoliteIdentityPublicKeyFile'] - @@old_repo_base = Setting.plugin_redmine_git_hosting['gitRepositoryBasePath'] - + @@old_valuehash = (Setting.plugin_redmine_git_hosting).clone def reload_this_observer observed_classes.each do |klass| @@ -16,34 +9,83 @@ def reload_this_observer end end + # There is a long-running bug in ActiveRecord::Observer that prevents us from + # returning from before_save() with false to signal verification failure. + # + # Thus, we can only silently refuse to perform bad changes and/or perform + # slight corrections to badly formatted values. def before_save(object) - if object.name == "plugin_redmine_git_hosting" && !GitHosting.bin_dir_writeable? - # If bin directory not alterable, don't alow changes to - # Git Username, or Gitolite public or private keys + # Only validate settings for our plugin + if object.name == "plugin_redmine_git_hosting" valuehash = object.value - valuehash['gitUser'] = @@old_git_user - valuehash['gitoliteIdentityFile'] = @@old_gitolite_identity - valuehash['gitoliteIdentityPublicKeyFile'] = @@old_gitolite_publickey - object.value = valuehash + if !GitHosting.bin_dir_writeable? + # If bin directory not alterable, don't allow changes to + # Git Username, or Gitolite public or private keys + valuehash['gitUser'] = @@old_valuehash['gitUser'] + valuehash['gitoliteIdentityFile'] = @@old_valuehash['gitoliteIdentityFile'] + valuehash['gitoliteIdentityPublicKeyFile'] = @@old_valuehash['gitoliteIdentityPublicKeyFile'] + end + + # Normalize Repository path, should be relative and end in '/' + if valuehash['gitRepositoryBasePath'] + normalizedFile = File.expand_path(valuehash['gitRepositoryBasePath'].lstrip.rstrip,"/") + if (normalizedFile != "/") + valuehash['gitRepositoryBasePath'] = normalizedFile[1..-1] + "/" # Clobber leading '/' add trailing '/' + else + valuehash['gitRepositoryBasePath'] = @@old_valuehash['gitRepositoryBasePath'] + end + end + + # Normalize Recycle bin path, should be relative and end in '/' + if valuehash['gitRecycleBasePath'] + normalizedFile = File.expand_path(valuehash['gitRecycleBasePath'].lstrip.rstrip,"/") + if (normalizedFile != "/") + valuehash['gitRecycleBasePath'] = normalizedFile[1..-1] + "/" # Clobber leading '/' add trailing '/' + else + valuehash['gitRecycleBasePath'] = @@old_valuehash['gitRecycleBasePath'] + end + end + + # Exclude bad expire times (and exclude non-numbers) + if valuehash['gitRecycleExpireTime'] + if valuehash['gitRecycleExpireTime'].to_f > 0 + valuehash['gitRecycleExpireTime'] = "#{(valuehash['gitRecycleExpireTime'].to_f * 10).to_i / 10.0}" + else + valuehash['gitRecycleExpireTime'] = @@old_valuehash['gitRecycleExpireTime'] + end + end + + # Validate wait time > 0 (and exclude non-numbers) + if valuehash['gitLockWaitTime'] + if valuehash['gitLockWaitTime'].to_i > 0 + valuehash['gitLockWaitTime'] = "#{valuehash['gitLockWaitTime'].to_i}" + else + valuehash['gitLockWaitTime'] = @@old_valuehash['gitLockWaitTime'] + end + end + # Save back results + object.value = valuehash end end def after_save(object) + # Only perform after-actions on settings for our plugin if object.name == "plugin_redmine_git_hosting" + valuehash = object.value if GitHosting.bin_dir_writeable? %x[ rm -rf '#{ GitHosting.get_tmp_dir }' ] %x[ rm -rf '#{ GitHosting.get_bin_dir }' ] end - if @@old_repo_base != object.value['gitRepositoryBasePath'] + if @@old_valuehash['gitRepositoryBasePath'] != valuehash['gitRepositoryBasePath'] GitHostingObserver.set_update_active(false) all_projects = Project.find(:all) all_projects.each do |p| if p.repository.is_a?(Repository::Git) r = p.repository repo_name= p.parent ? File.join(GitHosting::get_full_parent_path(p, true),p.identifier) : p.identifier - r.url = File.join(object.value['gitRepositoryBasePath'], "#{repo_name}.git") + r.url = File.join(valuehash['gitRepositoryBasePath'], "#{repo_name}.git") r.root_url = r.url r.save end @@ -51,24 +93,20 @@ def after_save(object) GitHostingObserver.set_update_active(true) end - if @@old_git_user != object.value['gitUser'] + if @@old_valuehash['gitUser'] != valuehash['gitUser'] GitHosting.setup_hooks - GitHosting.update_repositories( Project.find(:all), false) + GitHosting.update_repositories(:resync_all=>true) - elsif @@old_http_server != object.value['httpServer'] || @@old_hook_debug != object.value['gitHooksDebug'] || @@old_repo_base != object.value['gitRepositoryBasePath'] || @@old_hook_asynch != object.value['gitHooksAreAsynchronous'] + elsif @@old_valuehash['httpServer'] != valuehash['httpServer'] || + @@old_valuehash['gitHooksDebug'] != valuehash['gitHooksDebug'] || + @@old_valuehash['gitRepositoryBasePath'] != valuehash['gitRepositoryBasePath'] || + @@old_valuehash['gitHooksAreAsynchronous'] != valuehash['gitHooksAreAsynchronous'] GitHosting.update_global_hook_params end - @@old_hook_debug = object.value['gitHooksDebug'] - @@old_hook_asynch = object.value['gitHooksAreAsynchronous'] - @@old_http_server = object.value['httpServer'] - @@old_git_user = object.value['gitUser'] - @@old_gitolite_identity = object.value['gitoliteIdentityFile'] - @@old_gitolite_publickey = object.value['gitoliteIdentityPublicKeyFile'] - @@old_repo_base = object.value['gitRepositoryBasePath'] + @@old_valuehash = (Setting.plugin_redmine_git_hosting).clone end end - end diff --git a/app/models/gitolite_public_key.rb b/app/models/gitolite_public_key.rb index 1c36e36e1..809e4c5f2 100755 --- a/app/models/gitolite_public_key.rb +++ b/app/models/gitolite_public_key.rb @@ -32,4 +32,14 @@ def set_identifier end def to_s ; title ; end + + @@myregular = /^redmine_(.*)_\d*_\d*(.pub)?$/ + def self.ident_to_user_token(identifier) + result = @@myregular.match(identifier) + (result!=nil) ? result[1] : nil + end + + def self.user_to_user_token(user) + user.login.underscore.gsub(/[^0-9a-zA-Z\-]/,'_') + end end diff --git a/app/views/settings/_redmine_git_hosting.html.erb b/app/views/settings/_redmine_git_hosting.html.erb index 9e6527c84..dddb72792 100644 --- a/app/views/settings/_redmine_git_hosting.html.erb +++ b/app/views/settings/_redmine_git_hosting.html.erb @@ -48,6 +48,25 @@

+

+ + <%= text_field_tag("settings[gitRecycleBasePath]", @settings['gitRecycleBasePath'], :size => 60) %> +
+

+ +

+ + <%= text_field_tag("settings[gitRecycleExpireTime]", @settings['gitRecycleExpireTime'], :size => 10) %> +
+

+ +

+ + <%= text_field_tag("settings[gitLockWaitTime]", @settings['gitLockWaitTime'], :size => 10) %> +
+

+ +

<%= l(:label_git_cache_parameters)%>

diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/bs.yml b/config/locales/bs.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/bs.yml +++ b/config/locales/bs.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/da.yml b/config/locales/da.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/de.yml b/config/locales/de.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/el.yml b/config/locales/el.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/en.yml b/config/locales/en.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/es.yml b/config/locales/es.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/he.yml b/config/locales/he.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/id.yml b/config/locales/id.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/it.yml b/config/locales/it.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/no.yml b/config/locales/no.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index e9989cd0e..96dc0a6df 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -7,6 +7,9 @@ pt-BR: label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Ficheiro de identidade SSH Gitolite (Chave Privada) label_git_repository_base_path: 'Directório Git base (Relativo à "home" do utilizador git)' + label_git_recycle_base_path: 'Directório Recycle Bin base (Relativo à "home" do utilizador git)' + label_git_recycle_expire_time: 'expirar o tempo para Recycle Bin (em horas)' + label_git_lock_wait_time: 'Sincronização de bloqueio o tempo de espera (em segundos)' label_cannot_change_selinux: 'não pode ser mudada (selinux)' field_git_daemon: Git Daemon diff --git a/config/locales/pt.yml b/config/locales/pt.yml index f6db9e33d..cf033316f 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -7,6 +7,9 @@ pt: label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Ficheiro de identidade SSH Gitolite (Chave Privada) label_git_repository_base_path: 'Directório Git base (Relativo à "home" do utilizador git)' + label_git_recycle_base_path: 'Directório Recycle Bin base (Relativo à "home" do utilizador git)' + label_git_recycle_expire_time: 'expirar o tempo para Recycle Bin (em horas)' + label_git_lock_wait_time: 'Sincronização de bloqueio o tempo de espera (em segundos)' label_cannot_change_selinux: 'não pode ser mudada (selinux)' field_git_daemon: Git Daemon diff --git a/config/locales/ro.yml b/config/locales/ro.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/ro.yml +++ b/config/locales/ro.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/sr.yml b/config/locales/sr.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/th.yml b/config/locales/th.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 723401ce1..b8c50fb92 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -6,7 +6,10 @@ label_git_user: Git Username label_gitolite_identity_public_key_file: Gitolite SSH Identity File (Public Key) label_gitolite_identity_file: Gitolite SSH Identity File (Private Key) - label_git_repository_base_path: Git Repository Base Path (Relative to git user home) + label_git_repository_base_path: Git Repository Base Path
(Relative to git user home) + label_git_recycle_base_path: Git Recycle Bin Base Path
(Relative to git user home) + label_git_recycle_expire_time: Expire Time for Repositories in Recycle Bin
(in hours) + label_git_lock_wait_time: Synchronization Lock Wait Time
(in seconds) label_cannot_change_selinux: Cannot be changed under selinux field_git_daemon: Git Daemon diff --git a/db/migrate/20111123214911_add_settings_to_plugin.rb b/db/migrate/20111123214911_add_settings_to_plugin.rb new file mode 100644 index 000000000..dc801779c --- /dev/null +++ b/db/migrate/20111123214911_add_settings_to_plugin.rb @@ -0,0 +1,34 @@ +class AddSettingsToPlugin < ActiveRecord::Migration + def self.up + begin + # Add some new settings to settings page, if they don't exist + valuehash = (Setting.plugin_redmine_git_hosting).clone + valuehash['gitRecycleBasePath'] ||= 'recycle_bin/' + valuehash['gitRecycleExpireTime'] ||= '24.0' + valuehash['gitLockWaitTime'] ||= '10' + if (Setting.plugin_redmine_git_hosting != valuehash) + Setting.plugin_redmine_git_hosting = valuehash + say "Added redmine_git_hosting settings: 'gitRecycleBasePath', 'getRecycleExpireTime', 'getLockWaitTime'" + end + rescue + # ignore problems if plugin settings don't exist yet + end + end + + def self.down + begin + # Remove above settings from plugin page + valuehash = (Setting.plugin_redmine_git_hosting).clone + valuehash.delete('gitRecycleBasePath') + valuehash.delete('gitRecycleExpireTime') + valuehash.delete('gitLockWaitTime') + if (Setting.plugin_redmine_git_hosting != valuehash) + Setting.plugin_redmine_git_hosting = valuehash + say "Removed redmine_git_hosting settings: 'gitRecycleBasePath', 'getRecycleExpireTime', 'getLockWaitTime'" + end + Setting.plugin_redmine_git_hosting = valuehash + rescue + # ignore problems if table doesn't exist yet.... + end + end +end diff --git a/init.rb b/init.rb index 721cdbaca..485b3dc77 100755 --- a/init.rb +++ b/init.rb @@ -12,11 +12,15 @@ description 'Enables Redmine / ChiliProject to control hosting of git repositories' version '0.4.2' url 'https://github.com/ericpaulbishop/redmine_git_hosting' + settings :default => { - 'httpServer' => 'localhost', - 'gitServer' => 'localhost', - 'gitUser' => 'git', + 'httpServer' => "tessellation.cs.berkeley.edu/redmine", + 'gitServer' => 'tessellation.cs.berkeley.edu', + 'gitUser' => 'git-tess', 'gitRepositoryBasePath' => 'repositories/', + 'gitRecycleBasePath' => 'recycle_bin/', + 'gitRecycleExpireTime' => '24.0', + 'gitLockWaitTime' => '10', 'gitoliteIdentityFile' => RAILS_ROOT + '/.ssh/gitolite_admin_id_rsa', 'gitoliteIdentityPublicKeyFile' => RAILS_ROOT + '/.ssh/gitolite_admin_id_rsa.pub', 'allProjectsUseGit' => 'false', diff --git a/lib/git_hosting.rb b/lib/git_hosting.rb index 07aa7e0e3..829faa448 100755 --- a/lib/git_hosting.rb +++ b/lib/git_hosting.rb @@ -2,17 +2,34 @@ require 'net/ssh' require 'tempfile' require 'tmpdir' +require 'stringio' require 'gitolite_conf.rb' +require 'gitolite_recycle.rb' require 'git_adapter_hooks.rb' module GitHosting + LOCK_WAIT_IF_UNDEF = 10 # In case settings not migrated (normally from settings) + REPOSITORY_IF_UNDEF = "repositories/" # In case settings not migrated (normally from settings) + # Used to register errors when pulling and pushing the conf file + class GitHostingException < StandardError + end + + # Time in seconds to wait before giving up on acquiring the lock + def self.lock_wait_time + Setting.plugin_redmine_git_hosting['gitLockWaitTime'].to_i || LOCK_WAIT_IF_UNDEF + end + + # Repository base path (relative to git user home directory) + def self.repository_base + Setting.plugin_redmine_git_hosting['gitRepositoryBasePath'] || REPOSITORY_IF_UNDEF + end + + @@logger = nil def self.logger - # it may be useful to redefine this for some debugging purposes - # but by default, we're just going to use the default Rails logger - return Rails.logger + @@logger ||= MyLogger.new end @@web_user = nil @@ -31,7 +48,6 @@ def self.git_user Setting.plugin_redmine_git_hosting['gitUser'] end - @@mirror_pubkey = nil def self.mirror_push_public_key if @@mirror_pubkey.nil? @@ -124,7 +140,7 @@ def self.repository_name project end def self.repository_path project - return File.join(Setting.plugin_redmine_git_hosting['gitRepositoryBasePath'], repository_name(project)) + ".git" + return File.join(repository_base, repository_name(project)) + ".git" end def self.add_route_for_project(p) @@ -162,7 +178,7 @@ def self.get_bin_dir @@git_hosting_bin_dir ||= Rails.root.join("vendor/plugins/redmine_git_hosting/bin") if !File.directory?(@@git_hosting_bin_dir) - logger.error "Creating bin directory: #{@@git_hosting_bin_dir}, Owner #{web_user}" + logger.info "Creating bin directory: #{@@git_hosting_bin_dir}, Owner #{web_user}" %x[mkdir -p "#{@@git_hosting_bin_dir}"] %x[chmod 750 "#{@@git_hosting_bin_dir}"] %x[chown #{web_user} "#{@@git_hosting_bin_dir}"] @@ -195,14 +211,14 @@ def self.bin_dir_writeable?(*option) end def self.git_exec_path - return File.join(get_bin_dir(), "run_git_as_git_user") + return File.join(get_bin_dir, "run_git_as_git_user") end def self.gitolite_ssh_path - return File.join(get_bin_dir(), "gitolite_admin_ssh") + return File.join(get_bin_dir, "gitolite_admin_ssh") end def self.git_user_runner_path - return File.join(get_bin_dir(), "run_as_git_user") + return File.join(get_bin_dir, "run_as_git_user") end @@ -227,7 +243,7 @@ def self.git_user_runner def self.update_git_exec - logger.info "Setting up #{get_bin_dir()}" + logger.info "Setting up #{get_bin_dir}" gitolite_key=Setting.plugin_redmine_git_hosting['gitoliteIdentityFile'] File.open(gitolite_ssh_path(), "w") do |f| @@ -290,16 +306,14 @@ def self.update_git_exec File.chmod(0550, git_exec_path()) File.chmod(0550, gitolite_ssh_path()) File.chmod(0550, git_user_runner_path()) - %x[chown #{web_user} -R "#{@@git_hosting_bin_dir}"] + %x[chown #{web_user} -R "#{get_bin_dir}"] end - @@lock_file = nil def self.lock(retries) is_locked = false - local_dir = get_tmp_dir() if @@lock_file.nil? - @@lock_file=File.new(File.join(local_dir,'redmine_git_hosting_lock'),File::CREAT|File::RDONLY) + @@lock_file=File.new(File.join(get_tmp_dir,'redmine_git_hosting_lock'),File::CREAT|File::RDONLY) end while retries > 0 @@ -318,195 +332,451 @@ def self.unlock end end + def self.shell(command) + begin + my_command = "#{command} 2>&1" + result = %x[#{my_command}].chomp + code = $?.exitstatus + rescue Exception => e + result=e.message + code = -1 + end + if code != 0 + logger.error "Command failed (return #{code}): #{command}" + logger.error "#{result}" + raise GitHostingException, "Shell Error" + end + end + # Try to get a cloned version of gitolite-admin repository. + # + # This code tries to recover from a variety of errors which have been observed + # in the field, including a loss of the admin key and an empty top-level directory + # + # Return: false => have uncommitted changes + # true => directory on master + # + # This routine must only be called after acquisition of the lock + # + # John Kubiatowicz, 11/15/11 def self.clone_or_pull_gitolite_admin # clone/pull from admin repo - local_dir = get_tmp_dir() - if File.exists? "#{local_dir}/gitolite-admin" - logger.info "Fetching changes for #{local_dir}/gitolite-admin" - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' fetch] - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' merge FETCH_HEAD] - else - logger.info "Cloning gitolite-admin repository" - %x[env GIT_SSH=#{gitolite_ssh()} git clone #{git_user}@#{Setting.plugin_redmine_git_hosting['gitServer']}:gitolite-admin.git #{local_dir}/gitolite-admin] - end - %x[chmod 700 "#{local_dir}/gitolite-admin" ] - # Make sure we have our hooks setup - GitAdapterHooks.check_hooks_installed + repo_dir = File.join(get_tmp_dir,"gitolite-admin") + begin + if (File.exists? "#{repo_dir}") && (File.exists? "#{repo_dir}/.git") && (File.exists? "#{repo_dir}/keydir") && (File.exists? "#{repo_dir}/conf") + logger.info "Fetching changes from gitolite-admin repository to #{repo_dir}" + shell %[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{repo_dir}/.git' --work-tree='#{repo_dir}' fetch] + shell %[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{repo_dir}/.git' --work-tree='#{repo_dir}' merge FETCH_HEAD] + + # unmerged changes=> non-empty return + return_val = %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{repo_dir}/.git' --work-tree='#{repo_dir}' status --short].empty? + else + logger.info "Cloning gitolite-admin repository to #{repo_dir}" + shell %[rm -rf "#{repo_dir}"] + shell %[env GIT_SSH=#{gitolite_ssh()} git clone #{git_user}@#{Setting.plugin_redmine_git_hosting['gitServer']}:gitolite-admin.git #{repo_dir}] + return_val = true # on master, since fresh clone + end + shell %[chmod 700 "#{repo_dir}" ] + # Make sure we have our hooks setup + GitAdapterHooks.check_hooks_installed + + return return_val + rescue + # Hm.... perhaps we have some other sort of failure... + logger.error "Failure to access gitolite-admin repository. Attempting to fix..." + begin + logger.info " Reestablishing gitolite key" + shell %[cat #{Setting.plugin_redmine_git_hosting['gitoliteIdentityPublicKeyFile']} | #{GitHosting.git_user_runner} 'cat > ~/id_rsa.pub'] + shell %[#{GitHosting.git_user_runner} 'gl-setup ~/id_rsa.pub'] + shell %[#{GitHosting.git_user_runner} 'rm ~/id_rsa.pub'] + + logger.info " Deleting and recloning gitolite-admin to #{repo_dir}" + shell %[rm -r #{repo_dir}] unless !File.exists?(repo_dir) + shell %[env GIT_SSH=#{gitolite_ssh()} git clone #{git_user}@#{Setting.plugin_redmine_git_hosting['gitServer']}:gitolite-admin.git #{repo_dir}] + shell %[chmod 700 "#{repo_dir}" ] + # Make sure we have our hooks setup + GitAdapterHooks.check_hooks_installed + logger.info "Successfully restablished access to gitolite-admin repository!" + rescue + logger.error "Failure again. Probably requires human intervention" + raise GitHostingException, "Gitolite-admine Clone Failure" + end + end end - def self.move_repository(old_name, new_name) - old_path = File.join(Setting.plugin_redmine_git_hosting['gitRepositoryBasePath'], "#{old_name}.git") - new_path = File.join(Setting.plugin_redmine_git_hosting['gitRepositoryBasePath'], "#{new_name}.git") + # Commit Changes to the gitolite-admin repository. This assumes that repository exists + # (i.e. that a clone_or_fetch_gitolite_admin has already be called). + # + # This routine must only be called after acquisition of the lock + # + # John Kubiatowicz, 11/15/11 + def self.commit_gitolite_admin(*args) + resyncing = args && args.first # create tmp dir, return cleanly if, for some reason, we don't have proper permissions - local_dir = get_tmp_dir() + repo_dir = File.join(get_tmp_dir,"gitolite-admin") + + # commit / push changes to gitolite admin repo + begin + if (!resyncing) + logger.info "Committing changes to gitolite-admin repository" + message = "Updated by Redmine" + else + logger.info "Committing corrections to gitolite-admin repository" + message = "Updated by Redmine: Corrections discovered during RESYNC_ALL" + end + shell %[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{repo_dir}/.git' --work-tree='#{repo_dir}' add keydir/*] + shell %[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{repo_dir}/.git' --work-tree='#{repo_dir}' add conf/gitolite.conf] + shell %[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{repo_dir}/.git' --work-tree='#{repo_dir}' config user.email '#{Setting.mail_from}'] + shell %[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{repo_dir}/.git' --work-tree='#{repo_dir}' config user.name 'Redmine'] + shell %[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{repo_dir}/.git' --work-tree='#{repo_dir}' commit -a -m '#{message}'] + shell %[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{repo_dir}/.git' --work-tree='#{repo_dir}' push ] + rescue + logger.error "Problems committing changes to gitolite-admin repository!! Probably requires human intervention" + raise GitHostingException, "Gitlite-admin Commit Failure" + end + end + def self.move_repository(old_name, new_name) #lock - if !lock(5) + if !lock(lock_wait_time) + logger.error "git_hosting: move_repository() exited without acquiring lock!" return end - - # Make sure we have gitoite-admin cloned - clone_or_pull_gitolite_admin - - # rename in conf file - conf = GitoliteConfig.new(File.join(local_dir, 'gitolite-admin', 'conf', 'gitolite.conf')) - conf.rename_repo( old_name, new_name ) - conf.save + begin + # Make sure we have gitoite-admin cloned + clone_or_pull_gitolite_admin - # physicaly move the repo BEFORE committing/pushing conf changes to gitolite admin repo - %x[#{git_user_runner} 'mkdir -p "#{new_path}"'] - %x[#{git_user_runner} 'rmdir "#{new_path}"'] - %x[#{git_user_runner} 'mv "#{old_path}" "#{new_path}"'] + old_path = File.join(repository_base, "#{old_name}.git") + new_path = File.join(repository_base, "#{new_name}.git") + + logger.warn "Adjusting position of repository from '#{old_name}' to '#{new_name}' in gitolite.conf" + + # rename in conf file + conf = GitoliteConfig.new(File.join(get_tmp_dir, 'gitolite-admin', 'conf', 'gitolite.conf')) + conf.rename_repo( old_name, new_name ) + conf.save + + logger.warn " Moving repository from '#{old_path}' to '#{new_path}' in gitolite repository" + + # physicaly move the repo BEFORE committing/pushing conf changes to gitolite admin repo + prefix = new_name[/.*(?=\/)/] # Complete directory path (if exists) without trailing '/' + if prefix + # Has subdirectory. Must construct destination directory + repo_prefix = File.join(repository_base, prefix) + GitHosting.shell %[#{git_user_runner} mkdir -p '#{repo_prefix}'] + end + shell %[#{git_user_runner} 'mv "#{old_path}" "#{new_path}"'] + + # If any empty directories left behind, try to delete them. Ignore failure. + old_prefix = old_name[/.*?(?=\/)/] # Top-level old directory without trailing '/' + if old_prefix + repo_subpath = File.join(repository_base, old_prefix) + result = %x[#{GitHosting.git_user_runner} find '#{repo_subpath}' -type d ! -regex '.*\.git/.*' -empty -depth -delete -print].chomp.split("\n") + result.each { |dir| logger.warn " Removing empty repository subdirectory: #{dir}"} + end + + # Commit / push changes to gitolite admin repo + commit_gitolite_admin + + rescue GitHostingException + logger.error "git_hosting: move_repository() failed" + rescue => e + logger.error e.message + logger.error e.backtrace[0..4].join("\n") + logger.error "git_hosting: move_repository() failed" + end + # unlock + unlock() + end + + # Delete repository from specified project. + # + # We remove all redmine keys from the repository access rights. + # There are then three options: + # + # 1) The repo has non-redmine keys => we leave it alone + # 2) The repo has no keys left, but repository delete is not enabled + # => will leave repository alone with redmine_dummy_key + # 3) The repo has no keys left and repository delete is enabled + # => will delete repository + def self.delete_repository(project) + # Grab lock + if !lock(lock_wait_time) + logger.error "git_hosting: delete_repository() exited without acquiring lock!" + return + end - # commit / push changes to gitolite admin repo - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' add keydir/*] - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' add conf/gitolite.conf] - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' config user.email '#{Setting.mail_from}'] - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' config user.name 'Redmine'] - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' commit -a -m 'updated by Redmine' ] - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' push ] + begin + # Make sure we have gitolite-admin cloned + clone_or_pull_gitolite_admin - # unlock + repo_name = repository_name(project) + + conf = GitoliteConfig.new(File.join(get_tmp_dir, 'gitolite-admin', 'conf', 'gitolite.conf')) + + # Kill off redmine keys + conf.delete_redmine_keys repo_name + + if Setting.plugin_redmine_git_hosting['deleteGitRepositories'] == "true" + if conf.repo_has_no_keys? repo_name + logger.warn "Deleting repository '#{repo_name}' from gitolite.conf" + conf.delete_repo repo_name + GitoliteRecycle.move_repository_to_recycle repo_name + else + logger.warn "Repository '#{repo_name}' not deleted from gitolite.conf (non-redmine keys present and preserved)" + end + else + logger.warn "Deleting all redmine keys for repository '#{repo_name}' from gitolite.conf" + end + + conf.save + + # Commit / push changes to gitolite admin repo + commit_gitolite_admin + + rescue GitHostingException + logger.error "git_hosting: delete_repository() failed" + rescue => e + logger.error e.message + logger.error e.backtrace[0..4].join("\n") + logger.error "git_hosting: delete_repository() failed" + end unlock() - end + # Update keys for all members of projects of interest + # + # This code is entirely self-correcting for keys owned by users of the specified + # projects. It should work regardless of the history of steps that got us here. + # + # Note that this code has changed from the original. Now, we look at all keys owned + # by users in the specified projects to make sure that they are still live. We + # do this with a single pass through the keydir and do not rely on the "inactive" + # status to tell us that a key should be deleted. The reason is that weird + # synchronization issues (not entirely understood) can cause phantom keys to get left + # in the keydir which can really mess up gitolite. + # + # Also, when performing :resync_all, if the 'deleteGitRepositories' setting is 'true', + # then we will remove repositories in the gitolite.conf file that are identifiable as + # "redmine managed" (because they have one or more keys of the right form) but which + # are nolonger live for some reason (probably because the project was deleted). + # + # John Kubiatowicz, 11/15/11 + # + # Usage: + # + # 1) update_repositories(project) => update for specified project + # 2) update_repositories([list of projects]) => update all projects + # 3) update_repositories(:flag1=>true, :flag2 => false) + # + # Current flags: + # :resync_all => go through all redmine-maintained gitolite repos, + # clean up keydir, delete unused keys, clean up gitolite.conf @@recursionCheck = false - def self.update_repositories(projects, is_repo_delete) - + def self.update_repositories(*args) + flags = {} + args.each {|arg| flags.merge!(arg) if arg.is_a?(Hash)} + if flags[:resync_all] + logger.info "Executing RESYNC_ALL operation on gitolite configuration" + projects = Project.active.has_module(:repository).find(:all, :include => :repository) + else + projects = args.flatten.select{|p| p.is_a?(Project)} + end + git_projects = projects.uniq.select{|p| p.repository.is_a?(Repository::Git) } + return if git_projects.empty? if(defined?(@@recursionCheck)) if(@@recursionCheck) + # This shouldn't happen any more -- log as error + logger.error "git_hosting: update_repositories() exited with positive recursionCheck flag!" return end - end + end @@recursionCheck = true - logger.debug "Updating repositories..." - projects = (projects.is_a?(Array) ? projects : [projects]) - - - # Don't bother doing anything if none of the projects we've been handed have a Git repository - unless projects.detect{|p| p.repository.is_a?(Repository::Git) }.nil? - - + # Grab actual lock + if !lock(lock_wait_time) + logger.error "git_hosting: update_repositories() exited without acquiring lock!" + @@recursionCheck = false + return + end - #lock - if !lock(5) - @@recursionCheck = false - return + begin + # Make sure we have gitoite-admin cloned. + on_master = clone_or_pull_gitolite_admin + + # Get directory for the gitolite-admin + repo_dir = File.join(get_tmp_dir,"gitolite-admin") + + # Flag to indicate whether repo has changed. If we have uncommited changes, we will commit later. + changed = !on_master + + # logger.info "Updating keydirectory for projects: #{git_projects.join ', '}" + + keydir = File.join(repo_dir,"keydir") + old_keyhash = {} + Dir.foreach(keydir) do |keyfile| + user_token = GitolitePublicKey.ident_to_user_token(keyfile) + if !user_token.nil? + old_keyhash[user_token] ||= [] + old_keyhash[user_token] << keyfile + end + end + + git_projects.map{|proj| proj.member_principals.map(&:user).compact}.flatten.uniq.each do |cur_user| + active_keys = cur_user.gitolite_public_keys.active || [] + + # Remove old keys that happen to be left around + cur_token = GitolitePublicKey.user_to_user_token(cur_user) + old_keynames = old_keyhash[cur_token] || [] + cur_keynames = active_keys.map{|key| "#{key.identifier}.pub"} + (old_keynames - cur_keynames).each do |keyname| + filename = File.join(keydir,"#{keyname}") + logger.warn "Removing gitolite key: #{keyname}" + %x[git --git-dir='#{repo_dir}/.git' --work-tree='#{repo_dir}' rm keydir/#{keyname}] + changed = true + end + + # Remove inactive keys (will already be deleted by above code) + cur_user.gitolite_public_keys.inactive.each {|key| GitolitePublicKey.destroy(key.id)} + + # Add missing keys to the keydir + active_keys.each do |key| + keyname = "#{key.identifier}.pub" + unless old_keynames.index(keyname) + filename = File.join(keydir,"#{keyname}") + logger.info "Adding gitolite key: #{keyname}" + File.open(filename, 'w') {|f| f.write(key.key.gsub(/\n/,'')) } + changed = true + end + end + + # In preparation for resync_all, below + old_keyhash.delete(cur_token) + end + + # Remove keys for deleted users + if flags[:resync_all] + # All keys left in old_keyhash should be for users nolonger authorized for gitolite repos + old_keyhash.each_value do |keyname| + filename = File.join(keydir,"#{keyname}") + logger.warn "Removing orphan gitolite key: #{keyname}" + %x[git --git-dir='#{repo_dir}/.git' --work-tree='#{repo_dir}' rm keydir/#{keyname}] + changed = true + end end - - - # Make sure we have gitoite-admin cloned - clone_or_pull_gitolite_admin - - - local_dir = get_tmp_dir() - conf = GitoliteConfig.new(File.join(local_dir, 'gitolite-admin', 'conf', 'gitolite.conf')) + conf = GitoliteConfig.new(File.join(repo_dir, 'conf', 'gitolite.conf')) orig_repos = conf.all_repos new_repos = [] new_projects = [] - changed = false - - projects.select{|p| p.repository.is_a?(Repository::Git)}.each do |project| - - repo_name = repository_name(project) - - #check for delete -- if delete we can just - #delete repo, and ignore updating users/public keys - if is_repo_delete - if Setting.plugin_redmine_git_hosting['deleteGitRepositories'] == "true" - conf.delete_repo(repo_name) - end - else - #check whether we're adding a new repo - if orig_repos[ repo_name ] == nil - changed = true - add_route_for_project(project) - new_repos.push repo_name - new_projects.push project - - end - - - # fetch users - users = project.member_principals.map(&:user).compact.uniq - write_users = users.select{ |user| user.allowed_to?( :commit_access, project ) } - read_users = users.select{ |user| user.allowed_to?( :view_changesets, project ) && !user.allowed_to?( :commit_access, project ) } - - # write key files - users.map{|u| u.gitolite_public_keys.active}.flatten.compact.uniq.each do |key| - filename = File.join(local_dir, 'gitolite-admin/keydir',"#{key.identifier}.pub") - unless File.exists? filename - File.open(filename, 'w') {|f| f.write(key.key.gsub(/\n/,'')) } - changed = true - end - end - - # delete inactives - users.map{|u| u.gitolite_public_keys.inactive}.flatten.compact.uniq.each do |key| - filename = File.join(local_dir, 'gitolite-admin/keydir',"#{key.identifier}.pub") - if File.exists? filename - %x[git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' rm keydir/#{key.identifier}.pub] - changed = true - GitolitePublicKey.destroy(key.id) - end - end - - # update users - read_user_keys = [] - write_user_keys = [] - read_users.map{|u| u.gitolite_public_keys.active}.flatten.compact.uniq.each do |key| - read_user_keys.push key.identifier - end - write_users.map{|u| u.gitolite_public_keys.active}.flatten.compact.uniq.each do |key| - write_user_keys.push key.identifier - end - - #git daemon - if (project.repository.extra.git_daemon == 1 || project.repository.extra.git_daemon == nil ) && project.is_public - read_user_keys.push "daemon" - end - - conf.set_read_user repo_name, read_user_keys - conf.set_write_user repo_name, write_user_keys + + # Regenerate configuration file for projects of interest + # logger.info "Updating gitolite.conf for projects: #{git_projects.join ', '}" + git_projects.each do |proj| + repo_name = repository_name(proj) + + #check whether we're adding a new repo + if orig_repos[ repo_name ] == nil + changed = true + add_route_for_project(proj) + new_repos.push repo_name + new_projects.push proj + + # Attempt to recover repository from recycle_bin, if present + GitoliteRecycle.recover_repository_if_present repo_name + end + + # fetch users + users = proj.member_principals.map(&:user).compact.uniq + write_users = users.select{ |user| user.allowed_to?( :commit_access, proj ) } + read_users = users.select{ |user| user.allowed_to?( :view_changesets, proj ) && !user.allowed_to?( :commit_access, proj ) } + + # update users + read_user_keys = [] + write_user_keys = [] + read_users.map{|u| u.gitolite_public_keys.active}.flatten.compact.uniq.each do |key| + read_user_keys.push key.identifier + end + write_users.map{|u| u.gitolite_public_keys.active}.flatten.compact.uniq.each do |key| + write_user_keys.push key.identifier + end + + #git daemon + if (proj.repository.extra.git_daemon == 1 || proj.repository.extra.git_daemon == nil ) && proj.is_public + read_user_keys.push "daemon" end + + # Remove previous redmine keys, then add new keys + # By doing things this way, we leave non-redmine keys alone + conf.delete_redmine_keys repo_name + conf.add_read_user repo_name, read_user_keys + conf.add_write_user repo_name, write_user_keys + + # This is in preparation for full resync (below) + orig_repos.delete repo_name end + + # If resyncing, check for orphan repositories which still have redmine keys... + # At this point, orig_repos contains all repositories in original gitolite.conf + # which are not part of an active redmine project. There are four possibilities: + # + # 1) These repos do not have redmine keys => we leave them alone + # 2) They have both redmine keys and other (non-redmine) keys => remove redmine keys + # 3) They have only redmine keys, but repository delete is not enabled + # => remove redmine keys (will leave redmine_dummy_key when we save) + # 4) They have only redmine keys and repository delete is enabled => delete repository + # + # Finally, delete expired files from recycle bin. + if flags[:resync_all] + orig_repos.each_key do |repo_name| + if conf.is_redmine_repo? repo_name + # First, delete redmine keys for this repository + conf.delete_redmine_keys repo_name + if (Setting.plugin_redmine_git_hosting['deleteGitRepositories'] == "true") && (conf.repo_has_no_keys? repo_name) + logger.warn "Deleting orphan repository '#{repo_name}' from gitolite.conf" + conf.delete_repo repo_name + GitoliteRecycle.move_repository_to_recycle repo_name + else + logger.info "Deleting redmine keys for orphan repository '#{repo_name}' from gitolite.conf" + end + end + end + GitoliteRecycle.delete_expired_files + end if conf.changed? conf.save changed = true end - + if changed - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' add keydir/*] - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' add conf/gitolite.conf] - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' config user.email '#{Setting.mail_from}'] - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' config user.name 'Redmine'] - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' commit -a -m 'updated by Redmine' ] - %x[env GIT_SSH=#{gitolite_ssh()} git --git-dir='#{local_dir}/gitolite-admin/.git' --work-tree='#{local_dir}/gitolite-admin' push ] + # Have changes. Commit / push changes to gitolite admin repo + commit_gitolite_admin flags[:resync_all] end - + # Set post recieve hooks for new projects # We need to do this AFTER push, otherwise necessary repos may not be created yet if new_projects.length > 0 GitAdapterHooks.setup_hooks(new_projects) end - unlock() - end - @@recursionCheck = false + rescue GitHostingException + logger.error "git_hosting: update_repositories() failed" + rescue => e + logger.error e.message + logger.error e.backtrace[0..4].join("\n") + logger.error "git_hosting: update_repositories() failed" + end + unlock() + @@recursionCheck = false end - - def self.clear_cache_for_project(project) + def self.clear_cache_for_project(project) if project.is_a?(Project) project = project.identifier end @@ -539,5 +809,24 @@ def self.update_global_hook_params unlock() end end + + class MyLogger + # Prefix to error messages + ERROR_PREFIX = "***> " + + # For errors, add our prefix to all messages + def error(*progname, &block) + if block_given? + Rails.logger.error(*progname) { "#{ERROR_PREFIX}#{yield}".gsub(/\n/,"\n#{ERROR_PREFIX}") } + else + Rails.logger.error "#{ERROR_PREFIX}#{progname}".gsub(/\n/,"\n#{ERROR_PREFIX}") + end + end + + # Handle everything else with base object + def method_missing(m, *args, &block) + Rails.logger.send m, *args, &block + end + end end diff --git a/lib/git_hosting/patches/repositories_controller_patch.rb b/lib/git_hosting/patches/repositories_controller_patch.rb index 72e71c999..e4935d9bd 100644 --- a/lib/git_hosting/patches/repositories_controller_patch.rb +++ b/lib/git_hosting/patches/repositories_controller_patch.rb @@ -42,7 +42,7 @@ def edit_with_scm_settings end end - GitHosting.update_repositories(@project, false) if !@project.repository.nil? + GitHosting.update_repositories(@project) if !@project.repository.nil? GitHosting.setup_hooks(@project) if !@project.repository.nil? else diff --git a/lib/git_hosting/patches/repository_patch.rb b/lib/git_hosting/patches/repository_patch.rb index 6c3695250..0fc6cc62e 100644 --- a/lib/git_hosting/patches/repository_patch.rb +++ b/lib/git_hosting/patches/repository_patch.rb @@ -32,8 +32,8 @@ def fetch_changesets_with_disable_update # Do actual update fetch_changesets_without_disable_update - # Reenable updates to perform a single update - GitHostingObserver.set_update_active(true); + # Reenable updates to perform a sync of all projects + GitHostingObserver.set_update_active(:resync_all); end end diff --git a/lib/git_hosting/patches/sys_controller_patch.rb b/lib/git_hosting/patches/sys_controller_patch.rb index 6b35c86e5..cf473c949 100644 --- a/lib/git_hosting/patches/sys_controller_patch.rb +++ b/lib/git_hosting/patches/sys_controller_patch.rb @@ -8,8 +8,8 @@ def fetch_changesets_with_disable_update # Do actual update fetch_changesets_without_disable_update - # Reenable updates to perform a single update - GitHostingObserver.set_update_active(true); + # Perform the updating process on all projects + GitHostingObserver.set_update_active(:resync_all); end def self.included(base) diff --git a/lib/gitolite_conf.rb b/lib/gitolite_conf.rb index 1a57a3e88..3279ff7e8 100755 --- a/lib/gitolite_conf.rb +++ b/lib/gitolite_conf.rb @@ -1,10 +1,16 @@ module GitHosting class GitoliteConfig + DUMMY_REDMINE_KEY="redmine_dummy_key" + def initialize file_path @path = file_path load end + def logger + return GitHosting.logger + end + def save File.open(@path, "w") do |f| f.puts content @@ -39,6 +45,29 @@ def rename_repo old_name, new_name end end + # A repository is a "redmine" repository if it has redmine keys or no keys + # (latter case is checked, since we end up adding the DUMMY_REDMINE_KEY to + # a repository with no keys anyway.... + def is_redmine_repo? repo_name + repository(repo_name).rights.detect {|perm, users| users.detect {|key| is_redmine_key? key}} || (repo_has_no_keys? repo_name) + end + + def delete_redmine_keys repo_name + return if !@repositories[repo_name] + + repository(repo_name).rights.each do |perm, users| + users.delete_if {|key| is_redmine_key? key} + end + end + + def repo_has_no_keys? repo_name + !repository(repo_name).rights.detect {|perm, users| users.length > 0} + end + + def is_redmine_key? keyname + (GitolitePublicKey::ident_to_user_token(keyname) || keyname == DUMMY_REDMINE_KEY) ? true : false + end + def changed? @original_content != content end @@ -51,7 +80,6 @@ def all_repos return repos end - private def load @original_content = [] @@ -81,14 +109,15 @@ def repository repo_name def content content = [] - # To facilitate creation of repos, even when no users are defined - # always define at least one user -- specifically the admin - # user which has rights to modify gitolite-admin and control - # all repos. Since the gitolite-admin user can grant anyone - # any permission anyway, this isn't really a security risk. - # If no users are defined, this ensures the repo actually - # gets created, hence it's necessary. - admin_user = @repositories["gitolite-admin"].rights["RW+".to_sym][0] + # If no gitolite-admin user, something seriously wrong. Add it in with id_rsa. + # + # If this doesn't work for some reason, will be corrected at later time by + # gl-setup run. + if @repositories["gitolite-admin"].nil? + content << "repo\tgitolite-admin" + content << "tRW+\t=\tid_rsa" + content << "" + end @repositories.each do |repo, rights| content << "repo\t#{repo}" has_users=false @@ -99,7 +128,8 @@ def content end end if !has_users - content << "\tR\t=\t#{admin_user}" + # If no users, use dummy key to make sure repo created + content << "\tR\t=\t#{DUMMY_REDMINE_KEY}" end content << "" end diff --git a/lib/gitolite_recycle.rb b/lib/gitolite_recycle.rb new file mode 100755 index 000000000..e140054d2 --- /dev/null +++ b/lib/gitolite_recycle.rb @@ -0,0 +1,112 @@ +module GitHosting + # This class implements a basic recycle bit for repositories deleted from the gitolite repository + # + # Whenever repositories are deleted, we rename them and place them in the recycle_bin. + # Assuming that GitoliteRecycle.delete_expired_files is called regularly, files in the recycle_bin + # older than 'preserve_time' will be deleted. Both the path for the recycle_bin and the preserve_time + # are settable as settings. + # + # John Kubiatowicz, 11/21/11 + class GitoliteRecycle + TRASH_DIR_SEP = "__" # Separator character(s) used to replace '/' in name + + RECYCLE_BIN_IF_UNDEF = "recycle_bin/" # In case settings not migrated (normally from settings) + PRESERVE_TIME_IF_UNDEF = 1440 # In case settings not migrated (normally from settings) + + def self.logger + return GitHosting.logger + end + + # Repository base path (relative to git user home directory) + def self.repository_base + Setting.plugin_redmine_git_hosting['gitRepositoryBasePath'] + end + + # Recycle bin base path (relative to git user home directory) + def self.recycle_bin + Setting.plugin_redmine_git_hosting['gitRecycleBasePath'] || RECYCLE_BIN_IF_UNDEF + end + + # Recycle preservation time (in minutes) + def self.preserve_time + (Setting.plugin_redmine_git_hosting['gitRecycleExpireTime'].to_f * 60).to_i || PRESERVE_TIME_IF_UNDEF + end + + # Scan through the recyclebin and delete files older than 'preserve_time' minutes + # This can fail silently if, for instance, the recycle_bin doesn't exist. That is the intended behavior. + def self.delete_expired_files + result = %x[#{GitHosting.git_user_runner} 'find #{recycle_bin} -type d -regex '.*\.git' -cmin +#{preserve_time} -prune -print'].chomp.split("\n") + if result.length > 0 + logger.warn "Garbage-collecting expired file#{(result.length != 1) ? "s" : ""} from recycle bin:" + result.each do |filename| + begin + GitHosting.shell %[#{GitHosting.git_user_runner} rm -r #{filename}] + logger.warn " Deleting #{filename}" + rescue + logger.error "GitoliteRecycle.delete_expired_files() failed trying to delete repository #{filename}!" + end + end + + # Optionally remove recycle_bin (but only if empty). Ignore error if non-empty + %x[#{GitHosting.git_user_runner} rmdir #{recycle_bin}] + end + end + + def self.move_repository_to_recycle repo_name + repo_path = File.join(repository_base, repo_name) + new_path = File.join(recycle_bin,"#{Time.now.to_i.to_s}#{TRASH_DIR_SEP}#{name_to_recycle_name(repo_name)}") + begin + GitHosting.shell %[#{GitHosting.git_user_runner} mkdir -p '#{recycle_bin}'] + GitHosting.shell %[#{GitHosting.git_user_runner} mv '#{repo_path}.git' '#{new_path}.git'] + logger.warn " Moving '#{repo_name}.git' from gitolite repository => '#{new_path}.git'. Will remain for at least #{preserve_time/60.0} hours" + # If any empty directories left behind, try to delete them. Ignore failure. + old_prefix = repo_name[/.*?(?=\/)/] # Top-level old directory without trailing '/' + if old_prefix + repo_subpath = File.join(repository_base, old_prefix) + result = %x[#{GitHosting.git_user_runner} find '#{repo_subpath}' -type d ! -regex '.*\.git/.*' -empty -depth -delete -print].chomp.split("\n") + result.each { |dir| logger.warn " Removing empty repository subdirectory: #{dir}"} + end + rescue + logger.error "Attempt to move repository '#{repo_name}.git' to recycle bin failed" + end + end + + def self.recover_repository_if_present repo_name + # Pull up any matching repositories. Sort them (beginning is representation of time) + myregex = File.join(recycle_bin,"[0-9]+#{TRASH_DIR_SEP}#{name_to_recycle_name(repo_name)}.git") + files = %x[#{GitHosting.git_user_runner} find '#{recycle_bin}' -type d -regex '#{myregex}' -prune].chomp.split("\n").sort {|x,y| y <=> x } + if files.length > 0 + # Found something! + logger.warn "Restoring '#{repo_name}.git' to gitolite repository from recycle bin (#{files.first})" + begin + prefix = repo_name[/.*(?=\/)/] # Complete directory path (if exists) without trailing '/' + if prefix + repo_prefix = File.join(repository_base, prefix) + # Has subdirectory. Must reconstruct directory + GitHosting.shell %[#{GitHosting.git_user_runner} mkdir -p '#{repo_prefix}'] + end + repo_path = File.join(repository_base, repo_name) + GitHosting.shell %[#{GitHosting.git_user_runner} mv '#{files.first}' '#{repo_path}.git'] + + # Optionally remove recycle_bin (but only if empty). Ignore error if non-empty + %x[#{GitHosting.git_user_runner} rmdir #{recycle_bin}] + return true + rescue + logger.error "Attempt to recover '#{repo_name}.git' failed" + return false + end + else + false + end + end + + # This routine takes a name and turns it into a name for the recycle bit, + # where we have a 1-level directory full of deleted repositories which + # we keep for 'preserve_time'. + def self.name_to_recycle_name repo_name + new_trash_name = "#{repo_name}".gsub(/\//,"#{TRASH_DIR_SEP}") + end + + + end +end diff --git a/tasks/gitolite.rake b/tasks/gitolite.rake index e15d136d3..8b3b1715f 100644 --- a/tasks/gitolite.rake +++ b/tasks/gitolite.rake @@ -1,11 +1,26 @@ +# There are two tasks here of interest: gitolite:update_repositories and gitolite:fetch_changesets. +# The second includes the first (since fetching of changesets causes updating of gitolite config). +# +# As of the most recent release, either of these will complete resynchronize the gitolite configuration +# and can thus be used to recover from errors that might have been introduced by sychronization errors. +# +# Specifically: +# +# 1) Resynchronize gitolite configuration (fix keys directory and configuration). Also, expire +# repositories in the recycle_bin if time. +# +# rake gitolite:update_repositories RAILS_ENV=xxx +# +# 2) Fetch all changesets for repositories and then rescynronize gitolite configuration (as in #1) +# +# rake gitolite:fetch_changes RAILS_ENV=xxx +# namespace :gitolite do - desc "update gitolite repositories" + desc "Update all gitolite repositories" task :update_repositories => [:environment] do - projects = Project.active - puts "Updating repositories for projects #{projects.join(' ')}" - GitHosting.update_repositories(projects, false) + GitHosting.update_repositories(:resync_all => true) end - desc "fetch commits from gitolite repositories" + desc "Fetch commits from gitolite repositories/update gitolite configuration" task :fetch_changes => [:environment] do Repository.fetch_changesets end From 01e8b32412eaceefe87bfdbbf51448f1b6bdff82 Mon Sep 17 00:00:00 2001 From: John Kubiatowicz Date: Sun, 27 Nov 2011 09:49:47 -0800 Subject: [PATCH 05/11] Missed a performance case/source of recursion complaints in the log. This is not a major issue (or even correctness issue), but will cause complaints of recursion in the log. --- app/controllers/gitolite_public_keys_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/gitolite_public_keys_controller.rb b/app/controllers/gitolite_public_keys_controller.rb index 1276f8471..131d2af40 100644 --- a/app/controllers/gitolite_public_keys_controller.rb +++ b/app/controllers/gitolite_public_keys_controller.rb @@ -18,12 +18,14 @@ def delete end def update + GitHostingObserver.set_update_active(false) if @gitolite_public_key.update_attributes(params[:public_key]) flash[:notice] = l(:notice_public_key_updated) redirect_to url_for(:controller => 'my', :action => 'account') else render :action => 'edit' end + GitHostingObserver.set_update_active(true) end def create From fffa1fd579161ffb4071f975b4476120f1fcdd4b Mon Sep 17 00:00:00 2001 From: John Kubiatowicz Date: Tue, 3 Jan 2012 21:05:53 -0800 Subject: [PATCH 06/11] Update of selinux policy for latest version of selinux package. Also change rakefile to install binary policy (redmine_git.pp) instead of compiling source policy (redmine_git.te) before every installation. New rake task added to recompile policy if necessary: rake selinux:redmine_git_hosting:build_policy --- selinux/README | 14 +++++++++++--- selinux/redmine_git.pp | Bin 127822 -> 128153 bytes selinux/redmine_git.te | 10 ++++++++-- tasks/selinux.rake | 10 ++++++++-- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/selinux/README b/selinux/README index a877fb20b..b57a919ec 100644 --- a/selinux/README +++ b/selinux/README @@ -52,15 +52,23 @@ Somewhat less far-reaching options include: # install new selinux policy, and install selinux context for # the redmine_git_hosting plugin - rake selinux:redmine_git_hosting:install + rake selinux:redmine_git_hosting:install RAILS_ENV=production -Finally, for those who are hand-crafting their own file context: +For those who are hand-crafting their own file context: # Build bin directory with customized scripts for redmine_git_hosting # and install new selinux policy. No file contexts will be # installed (so that you must do customization afterwards). - rake selinux:redmine_git_hosting:install_scripts_and_policy + rake selinux:redmine_git_hosting:install_scripts_and_policy RAILS_ENV=production + +Finally, to rebuild the policy file (redmine_git.pp) from source (redmine_git.te), +you can type: + + # Rebuild redmine_git_hosting selinux policy from source + + rake selinux:redmine_git_hosting:build_policy RAILS_ENV=production + diff --git a/selinux/redmine_git.pp b/selinux/redmine_git.pp index bfe21e6945a9e61a536b6baf50f119f2006732a9..80895f79f0f1bb1e8a076256f6c605b863778ce1 100644 GIT binary patch delta 4706 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 U%tvY#^m%Uv>xRBfXCNp2zdwhul>h($ diff --git a/selinux/redmine_git.te b/selinux/redmine_git.te index 54b0a7aa1..36c73d2d8 100644 --- a/selinux/redmine_git.te +++ b/selinux/redmine_git.te @@ -8,10 +8,13 @@ require { type httpd_t, httpd_sys_script_t, httpd_sys_script_exec_t; type sudo_db_t; type httpd_redmine_git_script_t; + type httpd_redmine_git_script_exec_t; + type gitosis_var_lib_t; class process { setrlimit setfscreate }; class netlink_route_socket { write getattr read bind create nlmsg_read }; class capability { setuid sys_resource setgid }; - class dir { getattr search write}; + class dir { getattr search write write rename create reparent rmdir }; + class lnk_file unlink; } apache_content_template(redmine_git) @@ -51,10 +54,13 @@ allow httpd_redmine_git_script_t self:netlink_route_socket { write getattr read allow httpd_redmine_git_script_t self:process setrlimit; allow httpd_redmine_git_script_t sudo_db_t:dir { getattr search }; +# Capabilities required to manage gitolite repositories +allow httpd_redmine_git_script_t gitosis_var_lib_t:dir { rename create reparent rmdir }; +allow httpd_redmine_git_script_t gitosis_var_lib_t:lnk_file unlink; gitosis_read_lib_files(httpd_redmine_git_script_t) gitosis_manage_lib_files(httpd_redmine_git_script_t) -httpd_rw_stream_sockets(httpd_redmine_git_script_t) +apache_rw_stream_sockets(httpd_redmine_git_script_t) kernel_read_kernel_sysctls(httpd_redmine_git_script_t) logging_send_syslog_msg(httpd_redmine_git_script_t) diff --git a/tasks/selinux.rake b/tasks/selinux.rake index 92e1e4c7c..3eae4cfd5 100644 --- a/tasks/selinux.rake +++ b/tasks/selinux.rake @@ -153,8 +153,14 @@ namespace :selinux do desc "Install selinux tags and policy for redmine_git_hosting." task :install_policy => [:environment] do puts "[Installing selinux tags and policy for redmine_git_hosting:" - plugin_roots = redmine_roots("vendor/plugins/redmine_git_hosting") - sh "#{plugin_roots[0]}/selinux/redmine_git.sh" + sh "semodule -i #{Rails.root}/vendor/plugins/redmine_git_hosting/selinux/redmine_git.pp" + puts "DONE.]" + end + + desc "Build and install selinux tags and policy for redmine_git_hosting." + task :build_policy => [:environment] do + puts "[Building and installing selinux policy for redmine_git_hosting:" + sh "#{Rails.root}/vendor/plugins/redmine_git_hosting/selinux/redmine_git.sh" puts "DONE.]" end From 2f1c213c0ce41d024ad2b86585e4dea99c25b2c7 Mon Sep 17 00:00:00 2001 From: John Kubiatowicz Date: Tue, 10 Jan 2012 23:15:43 -0800 Subject: [PATCH 07/11] Continuing rewrite of redmine_git_hosting to rationalize path handling. It is important to remember to migrate_plugin settings, since there are new settings and one setting has changed a bit (i.e. httpServer should nolonger include a rails_root path). Thus, make sure to: rake db:migrate_plugins This commit includes a number of changes. First and foremost, the update_repositories() function now handles deletes and repository moves as well as all other changes. Consequently, :RESYNC_ALL will be able to recover from a variety of problems involving movement of repositories resulting from project parent changes. Note that changes in the parent for a subtree of projects works much better than before. Second, this commit adds new settings which allow (1) redmine-managed repositories to be focused in a specific subdirectory of the gitolite repository, (2) allow repositories to be named either hierarchically (default) or concentrated in a single directory (flat) independent of project parents, and (3) an extra path parameter which can be added to the http URL for smart http access of repositories. Third, added the "access" box at the top of the settings page which shows explicitly how settings affect access parameters. Random improvements include (1) rewrite of routes for smart HTTP to better handle changes in parentage (and avoid need to change routes as project parents change); (2) changes in settings now immediately trigger resulting changes in state (before, the settings cache got in the way); (3) all of the path functions (repository positions, ssh access, http access) have been concentrated into a small number of functions at the top of git_hosting.rb, rather than spread throughout the code; (4) continuing the changes started with the original performance fixes, cleaned up observer behavior triggered by changes in project membership and settings. --- README.mkd | 64 ++- app/controllers/git_http_controller.rb | 114 +++-- app/models/git_hosting_observer.rb | 73 ++- app/models/git_hosting_settings_observer.rb | 76 ++- app/views/projects/_git_urls.erb | 21 +- app/views/repositories/_git_urls.erb | 57 ++- app/views/settings/_display_access.html.erb | 19 + .../settings/_redmine_git_hosting.html.erb | 74 +-- assets/javascripts/git_url_display.js | 6 +- assets/stylesheets/application.css | 21 +- config/locales/bg.yml | 21 +- config/locales/bs.yml | 21 +- config/locales/ca.yml | 21 +- config/locales/cs.yml | 21 +- config/locales/da.yml | 21 +- config/locales/de.yml | 21 +- config/locales/el.yml | 21 +- config/locales/en.yml | 21 +- config/locales/es.yml | 21 +- config/locales/fi.yml | 21 +- config/locales/fr.yml | 21 +- config/locales/gl.yml | 21 +- config/locales/he.yml | 21 +- config/locales/hu.yml | 21 +- config/locales/id.yml | 21 +- config/locales/it.yml | 21 +- config/locales/ja.yml | 21 +- config/locales/ko.yml | 21 +- config/locales/lt.yml | 21 +- config/locales/nl.yml | 21 +- config/locales/no.yml | 21 +- config/locales/pl.yml | 21 +- config/locales/pt-BR.yml | 23 +- config/locales/pt.yml | 22 +- config/locales/ro.yml | 21 +- config/locales/ru.yml | 21 +- config/locales/sk.yml | 21 +- config/locales/sl.yml | 21 +- config/locales/sr.yml | 21 +- config/locales/sv.yml | 21 +- config/locales/th.yml | 21 +- config/locales/tr.yml | 21 +- config/locales/uk.yml | 21 +- config/locales/vi.yml | 21 +- config/locales/zh-TW.yml | 21 +- config/locales/zh.yml | 21 +- config/routes.rb | 33 +- ...20111220055819_add_settings_to_plugin_2.rb | 51 ++ init.rb | 13 +- lib/git_adapter_hooks.rb | 13 +- lib/git_hosting.rb | 466 ++++++++++-------- .../patches/projects_controller_patch.rb | 75 +-- .../patches/repositories_controller_patch.rb | 11 +- lib/gitolite_conf.rb | 25 + lib/gitolite_recycle.rb | 34 +- 55 files changed, 1461 insertions(+), 544 deletions(-) create mode 100644 app/views/settings/_display_access.html.erb create mode 100644 db/migrate/20111220055819_add_settings_to_plugin_2.rb diff --git a/README.mkd b/README.mkd index d9fe779be..69528e6c1 100755 --- a/README.mkd +++ b/README.mkd @@ -1,4 +1,4 @@ -# Redmine Git Hosting Plugin (v0.4.2x) +# Redmine Git Hosting Plugin (v0.4.3x) A ChiliProject / Redmine plugin which makes configuring your own git hosting easy. This plugin allows straightforward management of gitolite and associated public keys, the git daemon, and integrates code from Scott Schacon's "grack" utility @@ -99,40 +99,53 @@ root directory: **(6)** It is best to set several plugin variables BEFORE you run the db:migrate\_plugins task in step 7. In particular it is important -that the *httpServer*, *gitServer*, *gitUser*, *gitRepositoryBasePath*, *gitoliteIdentityFile* and *gitoliteIdentityPublicKeyFile* -variables are set correctly. To adjust these variables, open an editor and edit [redmine_rails_root]/vendor/plugins/redmine_git_hosting/init.rb file. -Starting on line 22 you will see the settings definitions you should edit. +that the *httpServer*, *gitServer*, *gitUser*, *gitoliteIdentityFile* and *gitoliteIdentityPublicKeyFile* +variables are set correctly. Others that should be set include *gitRepositoryBasePath*, *gitRedmineSubdir*, and *gitRepositoryHierarchy*; however, +the default values for these variables should be sufficient for most installations. +To adjust these variables, open an editor and edit [redmine_rails_root]/vendor/plugins/redmine_git_hosting/init.rb file. +Starting on line 22, you will see the settings definitions you should edit. -The *httpServer* variable should be set to the url used to access your Redmine site, e.g. www.my-own-personal-git-host-server.com. Note that if Redmine is not -installed in the site root this should include the path to your Redmine root, e.g. www.my-own-personal-git-host-server.com/path/to/redmine +The *httpServer* variable should be set to the hostname which will be used to access your Redmine site, e.g. www.my-own-personal-git-host-server.com. This variable +may optionally include a port using the ':portnum' syntax, i.e. www.my-own-person-git-host-server.com:8000. Unlike earlier versions of this plugin, this variable should +*not* include the path to your Redmine root. +The *gitServer* variable should be set to the hostname which will be used to access the gitolite repositories via ssh. In most configurations, this +variable will be identical to the *httpServer*, except for the fact that *gitServer* will never include an optional port number. -The *gitServer* will usually be the same as the the httpServer variable -- this is the server name to use to access the gitolite repositories via ssh. This should be -the hostname only, so this will be different from *httpServer* if Redmine is not installed in the site root. In other words, even if Redmine is installed in -www.my-own-personal-git-host-server.com/path/to/redmine, *gitServer* should be set to www.my-own-personal-git-host-server.com - - -The *gitUser* is the user under which gitolite is installed +The *gitUser* is the user under which gitolite is installed. +If you followed the above directions you will not need to modify the *gitoliteIdentityFile* or *gitoliteIdentityPublicKeyFile* variables -- these specify +the path to the private/public key files for accessing the gitolite admin repository. -The *gitRepositoryBasePath* is the path *relative to the git user root* where the repositories are located. This should always end in a file separator, e.g. '/'. -Since gitolite always uses repositories/ as the default place for repositories you probably shouldn't have to change this. +Although you can change the following three variables, their default values provide for a very reasonable installation: +The *gitRepositoryBasePath* is the path *relative to the git user root* where the repositories are located. This should always be non-empty and should end +in a file separator, e.g. '/'. Since gitolite always uses repositories/ as the default place for repositories you probably shouldn't have to change this. -If you followed the above directions you will not need to modify the *gitoliteIdentityFile* or *gitoliteIdentityPublicKeyFile* variables -- these specify -the path to the private/public key files for accessing the gitolite admin repository. +The *gitRedmineSubdir* is an optional subdirectory under the *gitRepositoryBasePath* which can be used for all plugin-managed repositories. Its default value +is the empty string (no special subdirectory). If you choose to set it, make sure that the resulting path ends in a file separator, e.g. '/'. +The *gitRepositoryHierarchy* variable is a boolean value which denotes whether or not the plugin-managed repositories are placed into a hierarchy that +mirrors the project hierarchy. Its value is either 'true' (default) or 'false'. These variables can be modified at a later time in the Administration => Plugins => Redmine Git Hosting Plugin configuration page. However to ensure that the database migration from your existing repositories goes smoothly it is best to modify these variables now. +As an example of the significance of the previous three variables, suppose that project-3 is a child of project-2 which is a child of project-1. +Assume *gitRepositoryBasePath* == "repository/" and *gitRedmineSubdir* == "projects". When *gitRepositoryHierachy* is 'true', project-3.git will be stored in +repository/projects/project-1/project-2/project-3.git, which will further be reflected in the ssh access URL of repository/projects/project-1/project-2/project-3.git. +In contrast, when *gitRepositoryHierarchy* is 'false', project-3.git will be stored directly under repository/projects -- regardless of the number and identity of +any parents that it might have. Note that the top of the settings page (Administration => Plugins => Redmine Git Hosting Plugin configuration page) provides +information about how your chosen configuration affects the storage locations and URLs for accessing projects. **(7)** Run the rake db:migrate\_plugins task to update the database. You will need to do this once for every rails environment you have configured (e.g. production, development, testing). For the production environment run: RAILS_ENV=production rake db:migrate_plugins +At this point, if you wish to utilize selinux to protect your installation, you should follow the instructions given in the "Selinux" section, below. + **(8)** Unless you want to access your repositories exclusively via Smart HTTP users will need to set a public key to connect via SSH. To do this, open a browser, login to ChiliProject/Redmine and follow the "My Account" Link in the upper right-hand corner of the page. The right-hand column contains controls for adding your public key(s). @@ -143,6 +156,9 @@ do not re-use the key you set as the gitolite admin key. **(9)** The plugin is now configured, but you may now want to set some additional settings on the Administration => Plugins => Redmine Git Hosting Plugin page. +The *gitLockWaitTime* represents the amount of time that the plugin will wait in attempting to acquire its internal synchronization lock before giving up. +You probably will not need to change this value. + *Automatically Initialize Git Repositories For New Projects* can be enabled to automatically create a new git repository every time you create a new project. You won't have to create the project, and then create the repository -- this will be done all it one step. However, if you have both git and svn (or hg, or cvs etc.) repositories, this may cause problems so it is disabled by default. @@ -153,11 +169,8 @@ the accidental loss of data. If this feature is enabled, the safety is turned of deleted in ChiliProject/Redmine. Note, however, that even when this feature is enabled, deleted repositories are placed into a "recycle_bin" for a configurable amount of time (defaulting to 24 hours) and can be recovered by recreating the project in Redmine with the same Identifier. Details are placed in the log. -The *gitRecycleBasePath* is the path *relative to the git user root* where deleted repositories are placed. This path should end in a path separator, e.g. '/'. -Deleted repositories are kept here for up to *gitRecycleExpireTime* hours (configurable, defaults to 24.0 hours). - -The *gitLockWaitTime* represents the amount of time that the plugin will wait in attempting to acquire its internal synchronization lock before giving up. -You probably will not need to change this value. +The *Git Recycle Bin Base Path* is the path *relative to the git user root* where deleted repositories are placed. This path should end in a path separator, +e.g. '/'. Deleted repositories are kept here for up to *gitRecycleExpireTime* hours (configurable, defaults to 24.0 hours). *Show Checkout URLs* can be disabled to hide the git URL bar in the repository tab. It is enabled by default. @@ -323,7 +336,14 @@ change options, then place your system back into enforcing mode. Alternatively, file and reinstall the plugin. Under normal operation, you will get one selinux complaint about /bin/touch in your log each time that you visit the plugin settings page. -This rakefile and selinux configuration has been primarily tested on Redhat Enterprise Linux version 6.x +One final comment: The selinux policy exists in binary form as selinux/redmine_git.pp. Should this policy +need to be rebuilt, an additional rake task exists which will build the policy from selinux/redmine_git.te: + + rake selinux:redmine_git_hosting:build_policy + +This task can be followed by the selinux:install task. + +The rakefile and selinux configuration has been primarily tested on Redhat Enterprise Linux version 6.x with apache and fcgi. Other configurations may require slight tweaking. ## Tested Configurations diff --git a/app/controllers/git_http_controller.rb b/app/controllers/git_http_controller.rb index b5faaba1a..cd42f97a6 100644 --- a/app/controllers/git_http_controller.rb +++ b/app/controllers/git_http_controller.rb @@ -10,71 +10,83 @@ class GitHttpController < ApplicationController before_filter :authenticate - def index - p1 = params[:p1] - p2 = params[:p2] - p3 = params[:p3] - proj_id = params[:id] - - - @git_http_repo_path = (params[:path]).gsub(/\.git$/, "") - - reqfile = p2 == "" ? p1 : ( p3 == "" ? p1 + "/" + p2 : p1 + "/" + p2 + "/" + p3); - - if p1 == "git-upload-pack" - service_rpc("upload-pack") - elsif p1 == "git-receive-pack" - service_rpc("receive-pack") - elsif p1 == "info" && p2 == "refs" - get_info_refs(reqfile) - elsif p1 == "HEAD" - get_text_file(reqfile) - elsif p1 == "objects" && p2 == "info" - if p3 != packs - get_text_file(reqfile) - else - get_info_packs(reqfile) - end - elsif p1 == "objects" && p2 != "pack" - get_loose_object(reqfile) - elsif p1 == "objects" && p2 == "pack" && p3.match(/\.pack$/) - get_pack_file(reqfile) - elsif p1 == "objects" && p2 == "pack" && p3.match(/\.idx$/) - get_idx_file(reqfile) - else - render_not_found - end - + if proj_path_split = (params[:project_path].match(/^(.*?([^\/]+))\.git$/)) + proj_id = proj_path_split[2] + project = Project.find_by_identifier(proj_id) + @git_http_repo_path = project_path = proj_path_split[1] + + if GitHosting.http_access_url(project) == project_path + p1 = params[:path][0] || "" + p2 = params[:path][1] || "" + p3 = params[:path][2] || "" + + # Full requested path from .git repo + reqfile = params[:path].join('/') + + # git http protocol + if p1 == "git-upload-pack" + service_rpc("upload-pack") + elsif p1 == "git-receive-pack" + service_rpc("receive-pack") + elsif p1 == "info" && p2 == "refs" + get_info_refs(reqfile) + elsif p1 == "HEAD" + get_text_file(reqfile) + elsif p1 == "objects" && p2 == "info" + if p3 != "packs" + get_text_file(reqfile) + else + get_info_packs(reqfile) + end + elsif p1 == "objects" && p2 != "pack" + get_loose_object(reqfile) + elsif p1 == "objects" && p2 == "pack" && p3.match(/\.pack$/) + get_pack_file(reqfile) + elsif p1 == "objects" && p2 == "pack" && p3.match(/\.idx$/) + get_idx_file(reqfile) + else + render_not_found + end + else + # repository URL doesn't match + render_not_found + end + return + end + # Wrong prefix or :base doesn't end in .git or doesn't at least have one char in base name + render_not_found end private def authenticate - is_push = params[:p1] == "git-receive-pack" + is_push = (params[:path][0] == "git-receive-pack") query_valid = false authentication_valid = true - project = Project.find(params[:id]) - repository = project != nil ? project.repository : nil - if(project != nil && repository !=nil) - if repository.extra[:git_http] == 2 || (repository.extra[:git_http] == 1 && is_ssl?) - query_valid = true - allow_anonymous_read = project.is_public - if is_push || (!allow_anonymous_read) - authentication_valid = false - authenticate_or_request_with_http_basic do |login, password| - user = User.find_by_login(login); - if user.is_a?(User) - if user.allowed_to?( :commit_access, project ) || ((!is_push) && user.allowed_to?( :view_changesets, project )) - authentication_valid = user.check_password?(password) + if proj_path_split = (params[:project_path].match(/^(.*?([^\/]+))\.git$/)) + project = Project.find_by_identifier(proj_path_split[2]) + repository = project != nil ? project.repository : nil + if(project != nil && repository !=nil) + if repository.extra[:git_http] == 2 || (repository.extra[:git_http] == 1 && is_ssl?) + query_valid = true + allow_anonymous_read = project.is_public + if is_push || (!allow_anonymous_read) + authentication_valid = false + authenticate_or_request_with_http_basic do |login, password| + user = User.find_by_login(login); + if user.is_a?(User) + if user.allowed_to?( :commit_access, project ) || ((!is_push) && user.allowed_to?( :view_changesets, project )) + authentication_valid = user.check_password?(password) + end end + authentication_valid end - authentication_valid end end end - end + end #if authentication failed, error already rendered #so, just render case where user queried a project diff --git a/app/models/git_hosting_observer.rb b/app/models/git_hosting_observer.rb index 9599a0b7c..d254e8e1f 100644 --- a/app/models/git_hosting_observer.rb +++ b/app/models/git_hosting_observer.rb @@ -13,45 +13,40 @@ def reload_this_observer end - def self.set_update_active(is_active) - case is_active - when Symbol then @@updating_active_flags[is_active] = true - when Hash then @updating_active_flags.merge(is_active) - when Project then @@cached_project_updates << is_active - end - - if !is_active + def self.set_update_active(*is_active) + if !is_active || !is_active.first @@updating_active_stack += 1 else - @@updating_active_stack -= 1 - if @@updating_active_stack < 0 - @@updating_active_stack = 0 - end - end - - if is_active && @@updating_active_stack == 0 - if @@cached_project_updates.length > 0 || !@@updating_active_flags.empty? + is_active.each do |item| + case item + when Symbol then @@updating_active_flags[item] = true + when Hash then @@updating_active_flags.merge!(item) + when Project then @@cached_project_updates |= [item] + end + end + + # If about to transition to zero and have something to run, do it + if @@updating_active_stack == 1 && (@@cached_project_updates.length > 0 || !@@updating_active_flags.empty?) @@cached_project_updates = @@cached_project_updates.flatten.uniq.compact GitHosting::update_repositories(@@cached_project_updates, @@updating_active_flags) - @@cached_project_updates = [] - @@updating_active_flags = {} - end - @@updating_active = true - else - @@updating_active = false - end - end + @@cached_project_updates = [] + @@updating_active_flags = {} + end - def before_destroy(object) - if object.is_a?(Repository::Git) - if Setting.plugin_redmine_git_hosting['deleteGitRepositories'] == "true" - GitHosting::delete_repository(object.project) - %x[#{GitHosting::git_user_runner} 'rm -rf #{object.url}' ] - end - GitHosting::clear_cache_for_project(object.project) + # Wait until after running update_repositories before releasing + @@updating_active_stack -= 1 + if @@updating_active_stack < 0 + @@updating_active_stack = 0 + end end + @@updating_active = (@@updating_active_stack == 0) end - + + # Register args for updating and then do it without allowing recursive calls + def self.bracketed_update_repositories(*args) + set_update_active(false) + set_update_active(*args) + end def after_create(object) if not object.is_a?(Project) @@ -73,15 +68,18 @@ def after_save(object) def after_destroy(object) - if !object.is_a?(Repository::Git) - update_repositories(object) + if object.is_a?(Repository::Git) + update_repositories(object,:delete=>true) + GitHosting::clear_cache_for_project(object.project) + else + update_repositories(object) end end protected - def update_repositories(object) + def update_repositories(object,*flags) projects = [] case object when Repository::Git then projects.push(object.project) @@ -90,11 +88,12 @@ def update_repositories(object) when Member then projects.push(object.project) when Role then projects = object.members.map(&:project).flatten.uniq.compact end - if(projects.length > 0) + if (projects.length > 0) if (@@updating_active) - GitHosting::update_repositories(projects) + GitHosting::update_repositories(projects,*flags) else @@cached_project_updates.concat(projects) + @@updating_active_flags.merge!(*flags) unless flags.empty? end end end diff --git a/app/models/git_hosting_settings_observer.rb b/app/models/git_hosting_settings_observer.rb index 884779cb6..e80e36156 100644 --- a/app/models/git_hosting_settings_observer.rb +++ b/app/models/git_hosting_settings_observer.rb @@ -25,7 +25,37 @@ def before_save(object) valuehash['gitoliteIdentityFile'] = @@old_valuehash['gitoliteIdentityFile'] valuehash['gitoliteIdentityPublicKeyFile'] = @@old_valuehash['gitoliteIdentityPublicKeyFile'] end + + # Server should not include any path components + if valuehash['gitServer'] + normalizedServer = valuehash['gitServer'].lstrip.rstrip.split('/').first + if (normalizedServer == '') + valuehash['gitServer'] = @@old_valuehash['gitServer'] + else + valuehash['gitServer'] = normalizedServer + end + end + + # Server should not include any path components + if valuehash['httpServer'] + normalizedServer = valuehash['httpServer'].lstrip.rstrip.split('/').first + if (normalizedServer == '') + valuehash['httpServer'] = @@old_valuehash['httpServer'] + else + valuehash['httpServer'] = normalizedServer + end + end + # Normalize http repository subdirectory path, should be either empty or relative and end in '/' + if valuehash['httpServerSubdir'] + normalizedFile = File.expand_path(valuehash['httpServerSubdir'].lstrip.rstrip,"/") + if (normalizedFile != "/") + valuehash['httpServerSubdir'] = normalizedFile[1..-1] + "/" # Clobber leading '/' add trailing '/' + else + valuehash['httpServerSubdir'] = '' + end + end + # Normalize Repository path, should be relative and end in '/' if valuehash['gitRepositoryBasePath'] normalizedFile = File.expand_path(valuehash['gitRepositoryBasePath'].lstrip.rstrip,"/") @@ -36,6 +66,16 @@ def before_save(object) end end + # Normalize Redmine Subdirectory path, should be either empty or relative and end in '/' + if valuehash['gitRedmineSubdir'] + normalizedFile = File.expand_path(valuehash['gitRedmineSubdir'].lstrip.rstrip,"/") + if (normalizedFile != "/") + valuehash['gitRedmineSubdir'] = normalizedFile[1..-1] + "/" # Clobber leading '/' add trailing '/' + else + valuehash['gitRedmineSubdir'] = '' + end + end + # Normalize Recycle bin path, should be relative and end in '/' if valuehash['gitRecycleBasePath'] normalizedFile = File.expand_path(valuehash['gitRecycleBasePath'].lstrip.rstrip,"/") @@ -72,41 +112,43 @@ def after_save(object) # Only perform after-actions on settings for our plugin if object.name == "plugin_redmine_git_hosting" valuehash = object.value + + # Settings cache doesn't seem to invalidate symbolic versions of Settings immediately, + # so, any use of Setting.plugin_redmine_git_hosting[] by things called during this + # callback will be outdated.... True for at least some versions of redmine plugin... + # + # John Kubiatowicz 12/21/2011 + if Setting.respond_to?(:check_cache) + # Clear out all cached settings. + Setting.check_cache + end if GitHosting.bin_dir_writeable? %x[ rm -rf '#{ GitHosting.get_tmp_dir }' ] %x[ rm -rf '#{ GitHosting.get_bin_dir }' ] end - if @@old_valuehash['gitRepositoryBasePath'] != valuehash['gitRepositoryBasePath'] - GitHostingObserver.set_update_active(false) - all_projects = Project.find(:all) - all_projects.each do |p| - if p.repository.is_a?(Repository::Git) - r = p.repository - repo_name= p.parent ? File.join(GitHosting::get_full_parent_path(p, true),p.identifier) : p.identifier - r.url = File.join(valuehash['gitRepositoryBasePath'], "#{repo_name}.git") - r.root_url = r.url - r.save - end - end - GitHostingObserver.set_update_active(true) + if @@old_valuehash['gitRepositoryBasePath'] != valuehash['gitRepositoryBasePath'] || + @@old_valuehash['gitRedmineSubdir'] != valuehash['gitRedmineSubdir'] || + @@old_valuehash['gitRepositoryHierarchy'] != valuehash['gitRepositoryHierarchy'] || + @@old_valuehash['gitUser'] != valuehash['gitUser'] + # Need to update everyone! + GitHostingObserver.bracketed_update_repositories(:resync_all) end if @@old_valuehash['gitUser'] != valuehash['gitUser'] GitHosting.setup_hooks - GitHosting.update_repositories(:resync_all=>true) elsif @@old_valuehash['httpServer'] != valuehash['httpServer'] || @@old_valuehash['gitHooksDebug'] != valuehash['gitHooksDebug'] || - @@old_valuehash['gitRepositoryBasePath'] != valuehash['gitRepositoryBasePath'] || + @@old_valuehash['gitRedmineSubdir'] != valuehash['gitRedmineSubdir'] || + @@old_valuehash['gitRepositoryHierarchy'] != valuehash['gitRepositoryHierarchy'] || @@old_valuehash['gitHooksAreAsynchronous'] != valuehash['gitHooksAreAsynchronous'] GitHosting.update_global_hook_params end - - @@old_valuehash = (Setting.plugin_redmine_git_hosting).clone + @@old_valuehash = valuehash.clone end end end diff --git a/app/views/projects/_git_urls.erb b/app/views/projects/_git_urls.erb index be2121e58..fa9c4388c 100644 --- a/app/views/projects/_git_urls.erb +++ b/app/views/projects/_git_urls.erb @@ -10,17 +10,22 @@