gitgolem 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ # Command for authorization. Checks authenticated (via sshd) user's access to given repository, if access granted creates repository if needed and calls git-shell.
2
+ class Golem::Command::Auth < Golem::Command::Base
3
+ # @private
4
+ USAGE = "user (defaults to GOLEM_USER env)\nused for authorizing users, called by sshd (via keys file), calls git-shell"
5
+ # Regexp to check for git commands.
6
+ RE_CMD = /\A\s*(git[ \-](upload-pack|upload-archive|receive-pack))\s+'([^.]+).git'/
7
+
8
+ # Run the command. Git command is read from ENV['SSH_ORIGINAL_COMMAND']. Set environment variables (for hooks) and call git-shell if access granted.
9
+ # @param [String] user the user to run as.
10
+ def run(user = ENV['GOLEM_USER'])
11
+ abort 'Please use git!' unless matches = (ENV['SSH_ORIGINAL_COMMAND'] || '').match(RE_CMD)
12
+ cmd, subcmd, repo = matches[1, 3]
13
+ abort 'You don\'t have permission!' unless Golem::Access.check(user, repo, subcmd)
14
+ command(:create_repository, repo) unless File.directory?(Golem::Config.repository_path(repo))
15
+ set_env(user, repo)
16
+ exec "git-shell", "-c", "#{cmd} '#{ENV['GOLEM_REPOSITORY_PATH']}'"
17
+ end
18
+
19
+ private
20
+ def set_env(user, repo)
21
+ {
22
+ 'GOLEM_USER' => user,
23
+ 'GOLEM_REPOSITORY_NAME' => repo,
24
+ 'GOLEM_REPOSITORY_PATH' => Golem::Config.repository_path(repo),
25
+ }.each {|k, v| ENV[k] = v}
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ # Command for clearing repositories, suitable for cron.
2
+ class Golem::Command::ClearRepositories < Golem::Command::Base
3
+ # @private
4
+ USAGE = "\nclear .git directories not found in database"
5
+
6
+ # Run the command. Removes every '*.git' directory in {Golem::Config.repository_base_path}
7
+ # unless {Golem::Access.repositories} includes repository. Calls {Golem::Command::DeleteRepository}.
8
+ def run
9
+ repos = Golem::Access.repositories
10
+ Dir.glob(Golem::Config.repository_base_path + '/*.git').each do |repo_path|
11
+ repo = File.basename(repo_path)[0..-5]
12
+ next if repos.include?(repo)
13
+ command :delete_repository, repo
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ # Command for creating a repository.
2
+ class Golem::Command::CreateRepository < Golem::Command::Base
3
+ include Golem::Command::ManageHooks
4
+ # @private
5
+ USAGE = "name\ncreate a repository and install hooks"
6
+
7
+ # Run the command. Installs hooks with {#install_hooks}.
8
+ # @param [String] name repository name.
9
+ def run(name)
10
+ path = Golem::Config.repository_path(name)
11
+ abort "Repository already exists!" if File.directory?(path)
12
+ pwd = Dir.pwd
13
+ Dir.mkdir(path, 0700)
14
+ Dir.chdir(path)
15
+ system('git --bare init ' + (verbose? ? '>&2' : '>/dev/null 2>&1'))
16
+ print "Repository #{path} created, installing hooks...\n" if verbose?
17
+ install_hooks(name)
18
+ Dir.chdir(pwd)
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ # Command for deleting a repository.
2
+ class Golem::Command::DeleteRepository < Golem::Command::Base
3
+ # @private
4
+ USAGE = "name\ndelete a specific repository"
5
+
6
+ # Run the command.
7
+ # @param [String] name repository name.
8
+ def run(name)
9
+ repo_path = Golem::Config.repository_path(name)
10
+ abort 'Repository not found!' unless File.directory?(repo_path)
11
+ system("rm -rf #{repo_path}")
12
+ print "Removed repository #{repo_path}\n" if verbose?
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ # Command for listing configuration variables.
2
+ class Golem::Command::Environment < Golem::Command::Base
3
+ # @private
4
+ USAGE = "\nlist configuration values"
5
+
6
+ # List configuration variables that are set.
7
+ def run
8
+ print "Configuration values:\n"
9
+ print Golem::Config.config_hash.collect {|k, v| "\t " + k.to_s + ": " + v.to_s}.join("\n") + "\n"
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # Command to save configuration file.
2
+ class Golem::Command::SaveConfig < Golem::Command::Base
3
+ # @private
4
+ USAGE = "\nsave the configuration file\nPLEASE NOTE: this may destroy (overwrite) your old config (and thus destroy your static database)"
5
+
6
+ # Run the command. Calls {Golem::Config.save!}.
7
+ def run
8
+ Golem::Config.save!
9
+ print "Config was saved to #{Golem::Config.cfg_path.to_s}\n" if verbose?
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # Command to setup the database schema (only useful for {Golem::DB::Pg}).
2
+ class Golem::Command::SetupDb < Golem::Command::Base
3
+ # @private
4
+ USAGE = "\nsetup database schema\nPLEASE NOTE: this is useful for postgres database only"
5
+
6
+ # Run the command. Calls {Golem::DB.setup}.
7
+ def run
8
+ Golem::DB.setup
9
+ print "Database schema is set up at #{Golem::Config.db.to_s}\n" if verbose?
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ # Command for updating hooks in repositories.
2
+ class Golem::Command::UpdateHooks < Golem::Command::Base
3
+ include Golem::Command::ManageHooks
4
+ # @private
5
+ USAGE = "\nupdate hooks in every repository (please note: deletes old hooks and symlinks new ones)"
6
+
7
+ # Run the command. It runs {#clear_hooks} and {#install_hooks} on every repository.
8
+ def run
9
+ Golem::Access.repositories.each do |repo|
10
+ clear_hooks(repo)
11
+ install_hooks(repo)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ # Command for updating the .ssh/authorized_keys file.
2
+ class Golem::Command::UpdateKeysFile < Golem::Command::Base
3
+ # @private
4
+ USAGE = "\nupdate authorized_keys file with values from database"
5
+ # Content mark to identify automatically updated part of file.
6
+ CONTENT_MARK = "# golem keys - do not place lines below, because the content gets rewritten (AND DO NOT EDIT THIS LINE!)"
7
+ # Default SSH(D) options to set if using command="" style keys file.
8
+ SSH_OPTS_COMMAND_DEFAULT = "no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty"
9
+
10
+ # Run the command. Old content is preserved, searched for {CONTENT_MARK}, and only content after the mark gets replaced.
11
+ def run
12
+ orig_content = File.exists?(Golem::Config.keys_file_path) ? File.read(Golem::Config.keys_file_path) : ""
13
+ new_content = if orig_content.match(Regexp.new('^' + Regexp.escape(CONTENT_MARK) + '$'))
14
+ orig_content.sub(Regexp.new('^' + Regexp.escape(CONTENT_MARK) + '$.*\z', Regexp::MULTILINE), CONTENT_MARK + "\n" + keys_str)
15
+ else
16
+ orig_content + "\n" + CONTENT_MARK + "\n" + keys_str
17
+ end
18
+ File.open(Golem::Config.keys_file_path, "w") {|f| f.write(new_content)}
19
+ end
20
+
21
+ private
22
+ def keys_str
23
+ Golem::Access.ssh_keys.collect {|user, keys| keys.collect {|key| keys_file_line(user, key)}.join("\n")}.join("\n") + "\n"
24
+ end
25
+
26
+ def keys_file_line(user, key)
27
+ first_part = if Golem::Config.keys_file_use_command
28
+ "command=\"#{Golem::Config.bin_dir + '/golem'} auth '#{user}'\""
29
+ else
30
+ "environment=\"GOLEM_USER=#{user}\""
31
+ end
32
+ ssh_opts = if Golem::Config.keys_file_ssh_opts.nil?
33
+ Golem::Config.keys_file_use_command ? ",#{SSH_OPTS_COMMAND_DEFAULT}" : ""
34
+ else
35
+ "," + Golem::Config.keys_file_ssh_opts.to_s
36
+ end
37
+ "#{first_part}#{ssh_opts} #{key}"
38
+ end
39
+ end
@@ -0,0 +1,112 @@
1
+ # Configuration management.
2
+ module Golem::Config
3
+ # List of paths config file is searched for.
4
+ CFG_PATHS = ["/usr/local/etc/golem/golem.conf.rb", "/usr/local/etc/golem.conf.rb", "/etc/golem/golem.conf.rb", "/etc/golem.conf.rb", "~/golem.conf.rb"]
5
+ # List of available config variable names.
6
+ CFG_VARS = [:db, :user_home, :repository_dir, :cfg_path, :base_dir, :bin_dir, :hooks_dir, :keys_file_use_command, :keys_file_ssh_opts]
7
+
8
+ # Auto configure Golem. Tries to find config file, if one can be found executes it, otherwise calls {configure}.
9
+ # @param [String] path path to config file.
10
+ def self.auto_configure(path = nil, &block)
11
+ path = if ENV.key?('GOLEM_CONFIG') && File.exists?(ENV['GOLEM_CONFIG'])
12
+ ENV['GOLEM_CONFIG']
13
+ elsif ENV.key?('GOLEM_BASE') && File.exists?(ENV['GOLEM_BASE'].to_s + "/golem.conf.rb")
14
+ ENV['GOLEM_BASE'].to_s + "/golem.conf.rb"
15
+ else
16
+ CFG_PATHS.find {|try_path| File.exists?(try_path)}
17
+ end unless File.exists?(path.to_s)
18
+ if File.exists?(path.to_s)
19
+ @auto_configure_path = path.to_s
20
+ @auto_configure_block = block
21
+ require path.to_s
22
+ end
23
+ configure path unless @vars #configure was not called or there was no config file
24
+ end
25
+
26
+ # Configure Golem with options given as argument, yield self then setting defaults.
27
+ # @overload configure(path, &block)
28
+ # @param [String] path path to config file (interpreted as <i>:cfg_path => path</i>).
29
+ # @overload configure(opts, &block)
30
+ # @param [Hash] opts options or single path .
31
+ # @option opts [String] :db db configuration (postgres url or 'static'),
32
+ # @option opts [String] :user_home (ENV['HOME']) path to user's home directory (needed to place .ssh/authorized_keys),
33
+ # @option opts [String] :repository_dir (user_home + '/repositories') path to repositories, may be relative to +user_home+,
34
+ # @option opts [String] :cfg_path (base_dir + '/golem.conf.rb') path config file,
35
+ # @option opts [String] :base_dir path to base, defaults to in order ENV['GOLEM_BASE'], basedir of config file (if exists), basedir of library,
36
+ # @option opts [String] :bin_dir (base_dir + '/bin') path to directory containing the executables,
37
+ # @option opts [String] :hooks_dir (base_dir + '/bin') path to directory containing hooks,
38
+ # @option opts [Boolean] :keys_file_use_command controls (false) the .ssh/authorized_keys file syntax (<i>command=""_ or _environment=""</i>), see {file:README#keys_file authorized_keys},
39
+ # @option opts [String] :keys_file_ssh_opts (nil) the ssh options to set in .ssh/authorized_keys file, see {file:README#keys_file authorized_keys}.
40
+ # @return [Config] self.
41
+ def self.configure(opts_or_path = nil, &block)
42
+ opts = opts_or_path.is_a?(Hash) ? opts_or_path : {:cfg_path => opts_or_path}
43
+ opts[:cfg_path] = @auto_configure_path if @auto_configure_path
44
+ @vars = opts.reject {|k, v| ! CFG_VARS.include?(k)}
45
+ @auto_configure_block.call(self) if @auto_configure_block
46
+ yield self if block_given?
47
+ self.user_home = ENV['HOME'] if user_home.nil? && ENV.key?('HOME')
48
+ self.repository_dir = user_home + "/repositories" unless repository_dir
49
+ unless base_dir
50
+ self.base_dir = if ENV.key?('GOLEM_BASE')
51
+ ENV['GOLEM_BASE']
52
+ elsif File.exists?(cfg_path.to_s)
53
+ File.dirname(cfg_path.to_s)
54
+ else
55
+ File.expand_path(File.dirname(__FILE__) + '/../..')
56
+ end
57
+ end
58
+ self.cfg_path = base_dir + '/golem.conf.rb' unless cfg_path
59
+ self.bin_dir = base_dir + '/bin' unless bin_dir
60
+ self.hooks_dir = base_dir + '/hooks' unless hooks_dir
61
+ self.keys_file_use_command = false unless keys_file_use_command
62
+ self
63
+ end
64
+
65
+ # Override +respond_to?+ to respond to +.config_var+ and +.config_var=+ (e.g. Golem::Config.db = 'static').
66
+ def self.respond_to?(sym)
67
+ CFG_VARS.include?(sym) || (sym.to_s.match(/=\z/) && CFG_VARS.include?(sym.to_s[0..-2].to_sym)) || super
68
+ end
69
+
70
+ # Override +method_missing+ to handle +.config_var+ and +.config_var=+ (e.g. Golem::Config.db = 'static').
71
+ def self.method_missing(sym, *args, &block)
72
+ auto_configure unless @vars
73
+ return @vars[sym] if CFG_VARS.include?(sym)
74
+ return @vars[sym.to_s[0..-2].to_sym] = args.first if sym.to_s.match(/=\z/) && CFG_VARS.include?(sym.to_s[0..-2].to_sym)
75
+ super
76
+ end
77
+
78
+ # Get configuration variables that is set (e.g. not +nil+).
79
+ # @return [Hash] configuration variables.
80
+ def self.config_hash
81
+ auto_configure unless @vars
82
+ @vars.reject {|k, v| v.nil?}
83
+ end
84
+
85
+ # Write configuration to file.
86
+ def self.save!
87
+ abort "No configuration path given!" unless cfg_path
88
+ File.open(cfg_path, 'w') {|f| f.write("Golem.configure do |cfg|\n" + config_hash.collect {|k, v| "\tcfg.#{k.to_s} = \"#{v.to_s}\""}.join("\n") + "\nend\n")}
89
+ end
90
+
91
+ # @return [String] path to +authorized_keys+ file.
92
+ def self.keys_file_path
93
+ user_home + "/.ssh/authorized_keys"
94
+ end
95
+
96
+ # @return [String] path to directory containing repositories.
97
+ def self.repository_base_path
98
+ (repository_dir[0..0] == "/" ? '' : user_home + '/') + repository_dir
99
+ end
100
+
101
+ # @param [String] repo repository name.
102
+ # @return [String] path to given repository.
103
+ def self.repository_path(repo)
104
+ repository_base_path + '/' + repo.to_s + '.git'
105
+ end
106
+
107
+ # @param [String] hook hook name.
108
+ # @return [String] path to given hook.
109
+ def self.hook_path(hook)
110
+ hooks_dir + "/" + hook.to_s
111
+ end
112
+ end
data/lib/golem/db.rb ADDED
@@ -0,0 +1,46 @@
1
+ # Database handling. See {Golem::DB::Pg} and {Golem::DB::Static}.
2
+ #
3
+ # A +db+ should respond to 4 methods: +users+, +repositories+, +ssh_keys+, +setup+.
4
+ # The first 3 should take a single hash argument (options) and return an array/hash of results, +setup+
5
+ # takes no arguments (it may use a block). These options should be supported:
6
+ # * +:fields+: list of fields the results should include,
7
+ # * +:return+: type of return value, if is +:array+ then results should be an array, hash (attribute name => value pairs) otherwise,
8
+ # * any other key: should be interpreted as conditions (e.g. <i>:user => "name"</i> should return objects whose +user+ attribute is _name_).
9
+ module Golem::DB
10
+ autoload :Pg, "golem/db/pg"
11
+ autoload :Static, "golem/db/static"
12
+
13
+ # Proxy for the used db.
14
+ # @return [Pg, Static] the db currently used.
15
+ def self.db
16
+ @db ||= case Golem::Config.db
17
+ when /\Apostgres:\/\// then Pg.new(Golem::Config.db)
18
+ when "static" then Static.new
19
+ else abort "Unknown DB (#{Golem::Config.db.to_s})."
20
+ end
21
+ end
22
+
23
+ # Forwards to proxy's users.
24
+ # @return [Array, Hash] results.
25
+ def self.users(opts = {})
26
+ db.users(opts)
27
+ end
28
+
29
+ # Forwards to proxy's repositories.
30
+ # @return [Array, Hash] results.
31
+ def self.repositories(opts = {})
32
+ db.repositories(opts)
33
+ end
34
+
35
+ # Forwards to proxy's ssh_keys.
36
+ # @return [Array, Hash] results.
37
+ def self.ssh_keys(opts = {})
38
+ db.ssh_keys(opts)
39
+ end
40
+
41
+ # Forwards to proxy's setup.
42
+ # @return [] depends on proxy.
43
+ def self.setup(&block)
44
+ db.setup(&block)
45
+ end
46
+ end
@@ -0,0 +1,69 @@
1
+ require 'pg'
2
+
3
+ # Postgres functionality. Requires +pg+.
4
+ class Golem::DB::Pg
5
+ # Initializes +PGConn+ connection.
6
+ # @param [String] db_url postgres url to connect to (e.g. +postgres://user:pw@host/db+).
7
+ def initialize(db_url)
8
+ @connection ||= ::PGconn.connect(*(db_url.match(/\Apostgres:\/\/([^:]+):([^@]+)@([^\/]+)\/(.+)\z/) {|m| [m[3], 5432, nil, nil, m[4], m[1], m[2]]}))
9
+ end
10
+
11
+ # Retrieve users.
12
+ # @param [Hash] opts options, see {Golem::DB}.
13
+ # @return [Array] list of users.
14
+ def users(opts = {})
15
+ opts[:table] = :users
16
+ get(opts)
17
+ end
18
+
19
+ # Retrieve repositories.
20
+ # @param [Hash] opts options, see {Golem::DB}.
21
+ # @return [Array] list of repotitories.
22
+ def repositories(opts = {})
23
+ opts[:table] = :repositories
24
+ get(opts)
25
+ end
26
+
27
+ # Retrieve ssh keys.
28
+ # @param [Hash] opts options, see {Golem::DB}.
29
+ # @return [Array] list of keys.
30
+ def ssh_keys(opts = {})
31
+ opts[:table] = "keys join users on keys.user_name=users.name"
32
+ get(opts)
33
+ end
34
+
35
+ # Setup schema.
36
+ # @return [PGRes] result.
37
+ def setup
38
+ @connection.exec(File.read(File.expand_path(File.dirname(__FILE__) + '/postgres.sql')))
39
+ end
40
+
41
+ private
42
+ def get(opts = {})
43
+ table = opts.delete(:table) || ''
44
+ fields = opts.delete(:fields) || ['*']
45
+ fields = [fields] unless fields.is_a?(Array)
46
+ order = opts.delete(:order)
47
+ limit = opts.delete(:limit)
48
+ ret_array = opts.delete(:return) == :array
49
+ sql = "SELECT #{fields.collect {|f| f.to_s}.join(', ')} FROM #{table.to_s}"
50
+ sql += " WHERE #{opts.keys.enum_for(:each_with_index).collect {|k, i| k.to_s + ' = $' + (i + 1).to_s}.join(' AND ')}" if opts.length > 0
51
+ sql += " ORDER BY #{order.to_s}" if order
52
+ sql += " LIMIT #{limit.to_s}" if limit
53
+ res = @connection.exec(sql, opts.values)
54
+ ret_fields = fields === ['*'] ? res.fields : fields
55
+ ret = res.collect do |row|
56
+ if ret_array
57
+ v = ret_fields.collect {|f| row[f.to_s]}
58
+ v.length == 1 ? v.first : v
59
+ else
60
+ ret_fields.inject({}) do |memo, field|
61
+ memo[field.to_sym] = row[field.to_s]
62
+ memo
63
+ end
64
+ end
65
+ end
66
+ res.clear
67
+ ret
68
+ end
69
+ end
@@ -0,0 +1,19 @@
1
+ CREATE TABLE users (
2
+ name varchar(32) NOT NULL PRIMARY KEY,
3
+ created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
4
+ updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
5
+ );
6
+
7
+ CREATE TABLE keys (
8
+ user_name varchar(32) NOT NULL REFERENCES users (name) ON DELETE no action ON UPDATE no action,
9
+ key varchar(1024) NOT NULL UNIQUE,
10
+ created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
11
+ updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
12
+ PRIMARY KEY (user_name, key)
13
+ );
14
+
15
+ CREATE TABLE repositories (
16
+ name varchar(32) NOT NULL PRIMARY KEY,
17
+ user_name varchar(32) NOT NULL REFERENCES users (name) ON DELETE no action ON UPDATE no action,
18
+ created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
19
+ );
@@ -0,0 +1,66 @@
1
+ # Static database for small installations. To use it, write:
2
+ # Golem.configure do |cfg|
3
+ # cfg.db = 'static'
4
+ # Golem::DB.setup do |db|
5
+ # db.add_user 'test_user'
6
+ # db.add_repository 'test_repository', 'test_user'
7
+ # db.add_key 'test_user', 'test_key'
8
+ # end
9
+ # end
10
+ class Golem::DB::Static
11
+ # Create database, initialize users, repositories and ssh_keys to [].
12
+ def initialize
13
+ @users, @repositories, @ssh_keys = [], [], []
14
+ end
15
+
16
+ # Retrieve users.
17
+ # @param [Hash] opts options, see {Golem::DB}.
18
+ # @return [Array] list of users.
19
+ def users(opts = {})
20
+ opts[:return] == :array ? @users.collect {|u| u[:name]} : @users
21
+ end
22
+
23
+ # Retrieve repositories.
24
+ # @param [Hash] opts options, see {Golem::DB}.
25
+ # @return [Array] list of repotitories.
26
+ def repositories(opts = {})
27
+ opts[:return] == :array ? @repositories.collect {|r| r[:name]} : @repositories
28
+ end
29
+
30
+ # Retrieve ssh keys.
31
+ # @param [Hash] opts options, see {Golem::DB}.
32
+ # @return [Array] list of keys.
33
+ def ssh_keys(opts = {})
34
+ opts[:return] == :array ? @ssh_keys.collect {|k| [k[:user_name], k[:key]]} : @ssh_keys
35
+ end
36
+
37
+ # Add user to database.
38
+ # @param [String] name username,
39
+ # @return [Array] list of users.
40
+ def add_user(name)
41
+ @users << {:name => name}
42
+ end
43
+
44
+ # Add repository to database.
45
+ # @param [String] name repository name,
46
+ # @param [String] user_name username.
47
+ # @return [Array] list of repositories.
48
+ def add_repository(name, user_name)
49
+ abort "Cannot add repository, user not found!" unless users(:return => :array).include?(user_name)
50
+ @repositories << {:name => name, :user_name => user_name}
51
+ end
52
+
53
+ # Add key to database.
54
+ # @param [String] user_name username,
55
+ # @param [String] key ssh key (e.g. +cat id_rsa.pub+).
56
+ # @return [Array] list of keys.
57
+ def add_key(user_name, key)
58
+ abort "Cannot add key, user not found!" unless users(:return => :array).include?(user_name)
59
+ @ssh_keys << {:user_name => user_name, :key => key}
60
+ end
61
+
62
+ # Setup database.
63
+ def setup(&block)
64
+ yield self
65
+ end
66
+ end