gitgolem 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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