gitolite-dtg 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gemtest ADDED
File without changes
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ rdoc/*
5
+ *.conf
6
+ coverage
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in gitolite.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,49 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ gitolite-dtg (0.0.1)
5
+ gratr19 (~> 0.4.4.1)
6
+ grit (~> 2.5.0)
7
+ hashery (~> 1.5.0)
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ blankslate (3.1.2)
13
+ diff-lcs (1.1.3)
14
+ forgery (0.5.0)
15
+ gratr19 (0.4.4.1)
16
+ grit (2.5.0)
17
+ diff-lcs (~> 1.1)
18
+ mime-types (~> 1.15)
19
+ posix-spawn (~> 0.3.6)
20
+ hashery (1.5.0)
21
+ blankslate
22
+ json (1.6.6)
23
+ mime-types (1.19)
24
+ multi_json (1.3.2)
25
+ posix-spawn (0.3.6)
26
+ rdoc (3.12)
27
+ json (~> 1.4)
28
+ rspec (2.9.0)
29
+ rspec-core (~> 2.9.0)
30
+ rspec-expectations (~> 2.9.0)
31
+ rspec-mocks (~> 2.9.0)
32
+ rspec-core (2.9.0)
33
+ rspec-expectations (2.9.1)
34
+ diff-lcs (~> 1.1.3)
35
+ rspec-mocks (2.9.0)
36
+ simplecov (0.6.2)
37
+ multi_json (~> 1.3)
38
+ simplecov-html (~> 0.5.3)
39
+ simplecov-html (0.5.3)
40
+
41
+ PLATFORMS
42
+ ruby
43
+
44
+ DEPENDENCIES
45
+ forgery (~> 0.5.0)
46
+ gitolite-dtg!
47
+ rdoc (~> 3.12)
48
+ rspec (~> 2.9.0)
49
+ simplecov (~> 0.6.2)
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2010-2011 Stafford Brunk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,58 @@
1
+ # gitolite-dtg #
2
+ Digital Technology Group, University of Cambridge
3
+
4
+ Forked from [wingrunr21/gitolite](https://github.com/wingrunr21/gitolite)
5
+
6
+ This gem provides a Ruby read-only interface to the [gitolite](https://github.com/sitaramc/gitolite) git backend system
7
+ (by parsing the configuration file found in the bare gitolite-admin repository). It aims to enable an application to query
8
+ gitolite repository permissions based on data written in the gitolite-admin repository.
9
+
10
+ This fork is designed to work as part of a Ruby authorization mechanism to gitolite repositories (see the related
11
+ gollum-dtg project for an example of how we use it).
12
+
13
+ Please see the upstream project for a Ruby API aiming to provide all management functionality (read and write) that is
14
+ available via the gitolite-admin repository (like SSH keys, adding/removing repositories, etc).
15
+
16
+
17
+ ## Requirements ##
18
+ * Ruby 1.8.x or 1.9.x
19
+ * a working [gitolite](https://github.com/sitaramc/gitolite) installation
20
+ * appropiate read permisions for the <tt>gitolite-admin</tt> bare repository
21
+
22
+ ## Installation ##
23
+
24
+ gem install gitolite-dtg
25
+
26
+ ## Usage ##
27
+
28
+ ### Load a gitolite-admin repo ###
29
+
30
+ require 'gitolite-dtg'
31
+ ga_repo = Gitolite::Dtg::GitoliteAdmin.new("/path/to/gitolite/repos/gitolite-admin.git")
32
+
33
+ This method can only be called on an existing gitolite-admin repo.
34
+
35
+ ## Caveats ##
36
+ ### Windows compatibility ###
37
+ The grit gem (which is used for under-the-hood git operations) does not currently support Windows. Until it does, gitolite will be unable to support Windows.
38
+
39
+ ### Group Ordering ###
40
+ When the gitolite backend parses the config file, it does so in one pass. Because of this, groups that are modified after being used do not see those changes reflected in previous uses.
41
+
42
+ For example:
43
+
44
+ @groupa = bob joe sue
45
+ @groupb = jim @groupa
46
+ @groupa = sam
47
+
48
+ Group b will contain the users <tt>jim, bob, joe, and sue</tt>
49
+
50
+ The gitolite gem, on the other hand, will <em>always</em> output groups so that all modifications are represented before it is ever used. For the above example, group b will be output with the following users: <tt>jim, bob, joe, sue, and sam</tt>. The groups in the config file will look like this:
51
+
52
+ @groupa = bob joe sue sam
53
+ @groupb = jim @groupa
54
+
55
+
56
+ ### Contributors ###
57
+ * Stafford Brunk - [wingrunr21](https://github.com/wingrunr21) (original developer of the API)
58
+ * Alexander Simonov - [simonoff](https://github.com/simonoff)
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+ require 'rspec/core/rake_task'
4
+
5
+ # Rake tasks from https://github.com/mojombo/rakegem/blob/master/Rakefile
6
+
7
+ # Helper Functions
8
+ def name
9
+ @name ||= Dir['*.gemspec'].first.split('.').first
10
+ end
11
+
12
+ def version
13
+ line = File.read("lib/#{name}/version.rb")[/^\s*VERSION\s*=\s*.*/]
14
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
15
+ end
16
+
17
+ # Standard tasks
18
+ require 'rcov'
19
+ RSpec::Core::RakeTask.new(:spec)
20
+ task :test => :spec
21
+ task :default => :spec
22
+
23
+ require 'rdoc/task'
24
+ Rake::RDocTask.new do |rdoc|
25
+ rdoc.rdoc_dir = 'rdoc'
26
+ rdoc.title = "#{name} #{version}"
27
+ rdoc.rdoc_files.include('README*')
28
+ rdoc.rdoc_files.include('lib/**/*.rb')
29
+ end
30
+
31
+ desc "Open an irb session preloaded with this library"
32
+ task :console do
33
+ sh "irb -rubygems -r ./lib/#{name}.rb"
34
+ end
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "gitolite-dtg/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "gitolite-dtg"
7
+ s.version = Gitolite::Dtg::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Stafford Brunk", "Lucian Carata"]
10
+ s.email = ["wingrunr21@gmail.com", "lc525@cam.ac.uk"]
11
+ s.homepage = "https://github.com/lc525/gitolite-dtg"
12
+ s.summary = %q{A Ruby gem based on wingrunr21/gitolite for querying the permisions of gitolite repositories.}
13
+ s.description = %q{This gem provides a Ruby read-only interface to the gitolite git backend system (by parsing the configuration file found in the bare gitolite-admin repository). It aims to enable permission queries based on data written in the gitolite-admin repository. This fork is designed to work as part of a Ruby authorization mechanism to gitolite repositories.}
14
+
15
+ s.rubyforge_project = "gitolite-dtg"
16
+
17
+ s.add_development_dependency "rspec", "~> 2.9.0"
18
+ s.add_development_dependency "forgery", "~> 0.5.0"
19
+ s.add_development_dependency "rdoc", "~> 3.12"
20
+ s.add_development_dependency "simplecov", "~> 0.6.2"
21
+ s.add_dependency "grit", "~> 2.5.0"
22
+ s.add_dependency "hashery", "~> 1.5.0"
23
+ s.add_dependency "gratr19", "~> 0.4.4.1"
24
+
25
+ s.files = `git ls-files`.split("\n")
26
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
28
+ s.require_paths = ["lib"]
29
+ end
@@ -0,0 +1,53 @@
1
+ module Gitolite
2
+ module Dtg
3
+ class Config
4
+ # Represents a group inside the gitolite configuration. The name and users
5
+ # options are all encapsulated in this class. All users are stored as
6
+ # Strings!
7
+ class Group
8
+ attr_accessor :name, :users
9
+
10
+ PREPEND_CHAR = '@'
11
+
12
+ def initialize(name)
13
+ # naively remove the prepend char
14
+ # I don't think you can have two of them in a group name
15
+ @name = name.gsub(PREPEND_CHAR, '')
16
+ @users = []
17
+ end
18
+
19
+ def empty!
20
+ @users.clear
21
+ end
22
+
23
+ def add_user(user)
24
+ return if has_user?(user)
25
+ @users.push(user.to_s).sort!
26
+ end
27
+
28
+ def add_users(*users)
29
+ fixed_users = users.flatten.map{ |u| u.to_s }
30
+ @users.concat(fixed_users).sort!.uniq!
31
+ end
32
+
33
+ def rm_user(user)
34
+ @users.delete(user.to_s)
35
+ end
36
+
37
+ def has_user?(user)
38
+ @users.include? user.to_s
39
+ end
40
+
41
+ def size
42
+ @users.length
43
+ end
44
+
45
+ def to_s
46
+ members = @users.join(' ')
47
+ name = "#{PREPEND_CHAR}#{@name}"
48
+ "#{name.ljust(20)}= #{members}\n"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,83 @@
1
+ module Gitolite
2
+ module Dtg
3
+ class Config
4
+ #Represents a repo inside the gitolite configuration. The name, permissions, and git config
5
+ #options are all encapsulated in this class
6
+ class Repo
7
+ ALLOWED_PERMISSIONS = /-|C|R|RW\+?(?:C?D?|D?C?)M?/
8
+
9
+ attr_accessor :permissions, :name, :config, :owner, :description
10
+
11
+ def initialize(name)
12
+ #Store the perm hash in a lambda since we have to create a new one on every deny rule
13
+ #The perm hash is stored as a 2D hash, with individual permissions being the first
14
+ #degree and individual refexes being the second degree. Both Hashes must respect order
15
+ @perm_hash_lambda = lambda { OrderedHash.new {|k,v| k[v] = OrderedHash.new{|k2, v2| k2[v2] = [] }} }
16
+ @permissions = Array.new.push(@perm_hash_lambda.call)
17
+
18
+ @name = name
19
+ @config = {} #git config
20
+ end
21
+
22
+ def clean_permissions
23
+ @permissions = Array.new.push(@perm_hash_lambda.call)
24
+ end
25
+
26
+ def add_permission(perm, refex = "", *users)
27
+ if perm =~ ALLOWED_PERMISSIONS
28
+ #Handle deny rules
29
+ if perm == '-'
30
+ @permissions.push(@perm_hash_lambda.call)
31
+ end
32
+
33
+ @permissions.last[perm][refex].concat users.flatten
34
+ @permissions.last[perm][refex].uniq!
35
+ else
36
+ raise InvalidPermissionError, "#{perm} is not in the allowed list of permissions!"
37
+ end
38
+ end
39
+
40
+ def set_git_config(key, value)
41
+ @config[key] = value
42
+ end
43
+
44
+ def unset_git_config(key)
45
+ @config.delete(key)
46
+ end
47
+
48
+ def to_s
49
+ repo = "repo #{@name}\n"
50
+
51
+ @permissions.each do |perm_hash|
52
+ perm_hash.each do |perm, list|
53
+ list.each do |refex, users|
54
+ repo += " " + perm.ljust(6) + refex.ljust(25) + "= " + users.join(' ') + "\n"
55
+ end
56
+ end
57
+ end
58
+
59
+ @config.each do |k, v|
60
+ repo += " config " + k + " = " + v + "\n"
61
+ end
62
+
63
+ repo
64
+ end
65
+
66
+ def gitweb_description
67
+ if @description.nil?
68
+ nil
69
+ else
70
+ desc = "#{@name} "
71
+ desc += "\"#{@owner}\" " unless @owner.nil?
72
+ desc += "= \"#{@description}\""
73
+ end
74
+ end
75
+
76
+ #Gets raised if a permission that isn't in the allowed
77
+ #list is passed in
78
+ class InvalidPermissionError < ArgumentError
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,267 @@
1
+ require 'tempfile'
2
+ require File.join(File.dirname(__FILE__), 'config', 'repo')
3
+ require File.join(File.dirname(__FILE__), 'config', 'group')
4
+
5
+ module Gitolite
6
+ module Dtg
7
+ class Config
8
+ attr_accessor :repos, :groups, :config_blob, :flat_groups, :special_groups
9
+
10
+ def initialize(config)
11
+ @special_groups = ["all","raven"]
12
+ @repos = {}
13
+ @groups = {}
14
+ @flat_groups = {}
15
+ @config_blob = config
16
+ process_config(config)
17
+ flatten_groups
18
+ end
19
+
20
+ #TODO: merge repo unless overwrite = true
21
+ def add_repo(repo, overwrite = false)
22
+ raise ArgumentError, "Repo must be of type Gitolite::Config::Repo!" unless repo.instance_of? Gitolite::Config::Repo
23
+ @repos[repo.name] = repo
24
+ end
25
+
26
+ def rm_repo(repo)
27
+ name = normalize_repo_name(repo)
28
+ @repos.delete(name)
29
+ end
30
+
31
+ def has_repo?(repo)
32
+ name = normalize_repo_name(repo)
33
+ @repos.has_key?(name)
34
+ end
35
+
36
+ def get_repo(repo)
37
+ name = normalize_repo_name(repo)
38
+ @repos[name]
39
+ end
40
+
41
+ def add_group(group, overwrite = false)
42
+ raise ArgumentError, "Group must be of type Gitolite::Config::Group!" unless group.instance_of? Gitolite::Config::Group
43
+ @groups[group.name] = group
44
+ end
45
+
46
+ def rm_group(group)
47
+ name = normalize_group_name(group)
48
+ @groups.delete(name)
49
+ end
50
+
51
+ def has_group?(group)
52
+ name = normalize_group_name(group)
53
+ @groups.has_key?(name)
54
+ end
55
+
56
+ def get_group(group)
57
+ name = normalize_group_name(group)
58
+ @groups[name]
59
+ end
60
+
61
+ def to_file(path=".", filename=@filename)
62
+ raise ArgumentError, "Path contains a filename or does not exist" unless File.directory?(path)
63
+
64
+ new_conf = File.join(path, filename)
65
+ File.open(new_conf, "w") do |f|
66
+ #Output groups
67
+ dep_order = build_groups_depgraph
68
+ dep_order.each {|group| f.write group.to_s }
69
+
70
+ gitweb_descs = []
71
+ @repos.sort.each do |k, v|
72
+ f.write "\n"
73
+ f.write v.to_s
74
+
75
+ gwd = v.gitweb_description
76
+ gitweb_descs.push(gwd) unless gwd.nil?
77
+ end
78
+
79
+ f.write "\n"
80
+ f.write gitweb_descs.join("\n")
81
+ end
82
+
83
+ new_conf
84
+ end
85
+
86
+ private
87
+ #Based on
88
+ #https://github.com/sitaramc/gitolite/blob/pu/src/gl-compile-conf#cleanup_conf_line
89
+ def cleanup_config_line(line)
90
+ #remove comments, even those that happen inline
91
+ line.gsub!(/^((".*?"|[^#"])*)#.*/) {|m| m=$1}
92
+
93
+ #fix whitespace
94
+ line.gsub!('=', ' = ')
95
+ line.gsub!(/\s+/, ' ')
96
+ line.strip
97
+ end
98
+
99
+ def process_config(config)
100
+
101
+
102
+ context = [] #will store our context for permissions or config declarations
103
+
104
+ #Read each line of our config
105
+ config.data.lines.each do |l|
106
+
107
+ line = cleanup_config_line(l)
108
+ next if line.empty? #lines are empty if we killed a comment
109
+
110
+ case line
111
+ #found a repo definition
112
+ when /^repo (.*)/
113
+ #Empty our current context
114
+ context = []
115
+
116
+ repos = $1.split
117
+ repos.each do |r|
118
+ context << r
119
+
120
+ @repos[r] = Repo.new(r) unless has_repo?(r)
121
+ end
122
+ #repo permissions
123
+ when /^(-|C|R|RW\+?(?:C?D?|D?C?)M?) (.* )?= (.+)/
124
+ perm = $1
125
+ refex = $2 || ""
126
+ users = $3.split
127
+
128
+ context.each do |c|
129
+ @repos[c].add_permission(perm, refex, users)
130
+ end
131
+ #repo git config
132
+ when /^config (.+) = ?(.*)/
133
+ key = $1
134
+ value = $2
135
+
136
+ context.each do |c|
137
+ @repos[c].set_git_config(key, value)
138
+ end
139
+ #group definition
140
+ when /^#{Group::PREPEND_CHAR}(\S+) = ?(.*)/
141
+ group = $1
142
+ users = $2.split
143
+
144
+ @groups[group] = Group.new(group) unless has_group?(group)
145
+ @groups[group].add_users(users)
146
+ #gitweb definition
147
+ when /^(\S+)(?: "(.*?)")? = "(.*)"$/
148
+ repo = $1
149
+ owner = $2
150
+ description = $3
151
+
152
+ #Check for missing description
153
+ raise ParseError, "Missing Gitweb description for repo: #{repo}" if description.nil?
154
+
155
+ #Check for groups
156
+ raise ParseError, "Gitweb descriptions cannot be set for groups" if repo =~ /@.+/
157
+
158
+ if has_repo? repo
159
+ r = @repos[repo]
160
+ else
161
+ r = Repo.new(repo)
162
+ add_repo(r)
163
+ end
164
+
165
+ r.owner = owner
166
+ r.description = description
167
+ when /^include "(.+)"/
168
+ #TODO: implement includes
169
+ #ignore includes for now
170
+ when /^subconf (\S+)$/
171
+ #TODO: implement subconfs
172
+ #ignore subconfs for now
173
+ else
174
+ raise ParseError, "'#{line}' cannot be processed"
175
+ end
176
+ end
177
+ end
178
+
179
+ def flatten_groups
180
+ @groups.each_value do |group|
181
+ @flat_groups[group.name]=flatten_group(group)
182
+ end
183
+ end
184
+
185
+ def flatten_group(group)
186
+ users = []
187
+ group.users.each do |user|
188
+ if user[0,1]=='@'
189
+ gname = user.gsub('@', '')
190
+ if ((@special_groups.include? gname) == false)
191
+ grp = @groups[gname]
192
+ gusr = flatten_group(grp)
193
+ if gusr != nil
194
+ users.concat(gusr)
195
+ end
196
+ end
197
+ else
198
+ users.push(user)
199
+ end
200
+ end
201
+ return users
202
+ end
203
+
204
+ # Normalizes the various different input objects to Strings
205
+ def normalize_name(context, constant = nil)
206
+ case context
207
+ when constant
208
+ context.name
209
+ when Symbol
210
+ context.to_s
211
+ else
212
+ context
213
+ end
214
+ end
215
+
216
+ def method_missing(meth, *args, &block)
217
+ if meth.to_s =~ /normalize_(\w+)_name/
218
+ #Could use Object.const_get to figure out the constant here
219
+ #but for only two cases, this is more readable
220
+ case $1
221
+ when "repo"
222
+ normalize_name(args[0], Gitolite::Dtg::Config::Repo)
223
+ when "group"
224
+ normalize_name(args[0], Gitolite::Dtg::Config::Group)
225
+ end
226
+ else
227
+ super
228
+ end
229
+ end
230
+
231
+ # Builds a dependency tree from the groups in order to ensure all groups
232
+ # are defined before they are used
233
+ def build_groups_depgraph
234
+ dp = ::GRATR::Digraph.new
235
+
236
+ # Add each group to the graph
237
+ @groups.each_value do |group|
238
+ dp.add_vertex! group
239
+
240
+ # Select group names from the users
241
+ subgroups = group.users.select {|u| u =~ /^#{Group::PREPEND_CHAR}.*$/}.map{|g| get_group g.gsub(Group::PREPEND_CHAR, '') }
242
+
243
+ subgroups.each do |subgroup|
244
+ dp.add_edge! subgroup, group
245
+ end
246
+ end
247
+
248
+ # Figure out if we have a good depedency graph
249
+ dep_order = dp.topsort
250
+
251
+ if dep_order.empty?
252
+ raise GroupDependencyError unless @groups.empty?
253
+ end
254
+
255
+ dep_order
256
+ end
257
+
258
+ #Raised when something in a config fails to parse properly
259
+ class ParseError < RuntimeError
260
+ end
261
+
262
+ # Raised when group dependencies cannot be suitably resolved for output
263
+ class GroupDependencyError < RuntimeError
264
+ end
265
+ end
266
+ end
267
+ end