gitolite-dtg 0.1.0

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,154 @@
1
+ require 'abbrev'
2
+
3
+ module Gitolite
4
+ module Dtg
5
+ class GitoliteAdmin
6
+ attr_accessor :gl_admin, :config
7
+
8
+ CONF = "gitolite.conf"
9
+ CONFDIR = "conf"
10
+ BRANCH = "master"
11
+
12
+ # Intialize with the path to
13
+ # the gitolite-admin repository
14
+ def initialize(path, options = {})
15
+ @path = path
16
+ @gl_admin = Grit::Repo.new(path)
17
+
18
+ @conf = options[:conf] || CONF
19
+ @confdir = options[:confdir] || CONFDIR
20
+ @branch = options[:branch] || BRANCH
21
+
22
+ # Load the configuration
23
+ load_data
24
+ end
25
+
26
+ # This method will destroy the in-memory data structures and reload everything
27
+ # from the file system
28
+ def reload!
29
+ load_data
30
+ end
31
+
32
+ #Checks to see if the given path is a gitolite-admin repository
33
+ #A valid repository contains a conf folder, keydir folder,
34
+ #and a configuration file within the conf folder
35
+ def self.is_gitolite_admin_repo?(dir)
36
+ # First check if it is a git repository
37
+ begin
38
+ repo = Grit::Repo.new(dir)
39
+ rescue Grit::InvalidGitRepositoryError
40
+ return false
41
+ end
42
+
43
+ # If we got here it is a valid git repo,
44
+ # now check directory structure
45
+ cbl = repo.tree / 'conf/gitolite.conf'
46
+ if cbl != nil
47
+ return true
48
+ else
49
+ return false
50
+ end
51
+ end
52
+
53
+ # repo_name - the repository name
54
+ # username - the authenticated user name
55
+ # resource_path - the path relative to the repository root that
56
+ # the user is requesting access to
57
+ # wanted_access - the type of access the user is requesting. can
58
+ # be 'R' or 'W'
59
+ def authorize(repo_name, username, resource_path, wanted_access)
60
+ if @config == nil
61
+ return false
62
+ end
63
+ repo = @config.repos[repo_name]
64
+ if repo != nil
65
+ repo.permissions.each do |perm_hash|
66
+ perm_hash.each do |perm, list|
67
+ #process a permission line
68
+ list.each do |refex, users|
69
+
70
+ ul = []
71
+ users.each do |user|
72
+ if user[0,1]=='@'
73
+ gname = user.gsub('@', '')
74
+ if ((@config.special_groups.include? gname) == false)
75
+ grp = @config.flat_groups[gname]
76
+ ul.concat(grp)
77
+ else
78
+ ul.push(user)
79
+ end
80
+ else
81
+ ul.push(user)
82
+ end
83
+ end
84
+ ul.uniq!
85
+
86
+ user_matches = false
87
+ if ((ul.include? "@all") || ((ul.include? "@raven") && (username != nil)) || (ul.include? username))
88
+ user_matches = true
89
+ end
90
+
91
+ if user_matches == false
92
+ next
93
+ end
94
+
95
+
96
+ refex_applies = false;
97
+ if refex == ''
98
+ refex_applies = true;
99
+ else
100
+ dirs = []
101
+ dirs.push(refex)
102
+ dirs.push(resource_path)
103
+
104
+ common_prefix = dirs.abbrev.keys.min_by {|key| key.length}.chop
105
+ common_directory = common_prefix.sub(%r{/[^/]*$}, '')
106
+
107
+ if common_directory != ''
108
+ refex_applies = true
109
+ end
110
+ end
111
+ if !refex_applies
112
+ next # if rule refex does not refer to the resource the user requested, go to the next rule
113
+ end
114
+
115
+
116
+ access_matches = false
117
+ if (perm.include? wanted_access)
118
+ access_matches = true
119
+ end
120
+
121
+ # authorization cases. at this point, user_matches==true and refex_applies==true:
122
+ if (perm == "-")
123
+ return false
124
+ elsif (user_matches && access_matches)
125
+ #print "Access allowed by matching rule: " + perm + " "
126
+ #print list
127
+ #print "\n"
128
+ return true
129
+ else
130
+ next
131
+ end
132
+
133
+ end
134
+ end
135
+ end
136
+ end
137
+ return false
138
+ end
139
+
140
+ private
141
+ def load_data
142
+ head = @gl_admin.commits(@branch).first
143
+ config_blob = head.tree / File.join(@confdir, @conf)
144
+ if config_blob != nil
145
+ @config = Config.new(config_blob)
146
+ else
147
+ @config = nil
148
+ print 'gitolite configuration could not be found in repository at ' + File.join(@path,@confdir,@conf)
149
+ end
150
+ end
151
+
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,5 @@
1
+ module Gitolite
2
+ module Dtg
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Gitolite
2
+ module Dtg
3
+ require 'grit'
4
+ require 'hashery'
5
+ require 'gratr'
6
+ require 'gitolite-dtg/config'
7
+ require 'gitolite-dtg/gitolite_admin'
8
+ end
9
+ end
@@ -0,0 +1,2 @@
1
+ #require 'gitolite'
2
+ #require 'spec_helper'
@@ -0,0 +1,459 @@
1
+ require 'gratr'
2
+ require 'gitolite/config'
3
+ require 'spec_helper'
4
+
5
+ describe Gitolite::Config do
6
+ conf_dir = File.join(File.dirname(__FILE__),'configs')
7
+
8
+ describe "#new" do
9
+ it 'should read a simple configuration' do
10
+ c = Gitolite::Config.new(File.join(conf_dir, 'simple.conf'))
11
+ c.repos.length.should == 2
12
+ c.groups.length.should == 0
13
+ end
14
+
15
+ it 'should read a complex configuration' do
16
+ c = Gitolite::Config.new(File.join(conf_dir, 'complicated.conf'))
17
+ c.groups.length.should == 5
18
+ c.repos.length.should == 12
19
+ end
20
+
21
+ describe 'gitweb operations' do
22
+ before :all do
23
+ @config = Gitolite::Config.new(File.join(conf_dir, 'complicated.conf'))
24
+ end
25
+
26
+ it 'should correctly read gitweb options for an existing repo' do
27
+ r = @config.get_repo('gitolite')
28
+ r.owner.should == "Sitaram Chamarty"
29
+ r.description.should == "fast, secure, access control for git in a corporate environment"
30
+ end
31
+
32
+ it 'should correctly read a gitweb option with no owner for an existing repo' do
33
+ r = @config.get_repo('foo')
34
+ r.owner.should be nil
35
+ r.description.should == "Foo is a nice test repo"
36
+ end
37
+
38
+ it 'should correctly read gitweb options for a new repo' do
39
+ r = @config.get_repo('foobar')
40
+ r.owner.should == "Bob Zilla"
41
+ r.description.should == "Foobar is top secret"
42
+ end
43
+
44
+ it 'should correctly read gitweb options with no owner for a new repo' do
45
+ r = @config.get_repo('bar')
46
+ r.owner.should be nil
47
+ r.description.should == "A nice place to get drinks"
48
+ end
49
+
50
+ it 'should raise a ParseError when a description is not specified' do
51
+ t = Tempfile.new('bad_conf.conf')
52
+ t.write('gitolite "Bob Zilla"')
53
+ t.close
54
+
55
+ lambda { Gitolite::Config.new(t.path) }.should raise_error(Gitolite::Config::ParseError)
56
+
57
+ t.unlink
58
+ end
59
+
60
+ it 'should raise a ParseError when a Gitweb description is specified for a group' do
61
+ t = Tempfile.new('bad_conf.conf')
62
+ t.write('@gitolite "Bob Zilla" = "Test description"')
63
+ t.close
64
+
65
+ lambda { Gitolite::Config.new(t.path) }.should raise_error(Gitolite::Config::ParseError)
66
+
67
+ t.unlink
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "#init" do
73
+ it 'should create a valid, blank Gitolite::Config' do
74
+ c = Gitolite::Config.init
75
+
76
+ c.should be_an_instance_of Gitolite::Config
77
+ c.repos.should_not be nil
78
+ c.repos.length.should be 0
79
+ c.groups.should_not be nil
80
+ c.groups.length.should be 0
81
+ c.filename.should == "gitolite.conf"
82
+ end
83
+
84
+ it 'should create a valid, blank Gitolite::Config with the given filename' do
85
+ filename = "test.conf"
86
+ c = Gitolite::Config.init(filename)
87
+
88
+ c.should be_an_instance_of Gitolite::Config
89
+ c.repos.should_not be nil
90
+ c.repos.length.should be 0
91
+ c.groups.should_not be nil
92
+ c.groups.length.should be 0
93
+ c.filename.should == filename
94
+ end
95
+ end
96
+
97
+ describe "repo management" do
98
+ before :each do
99
+ @config = Gitolite::Config.new(File.join(conf_dir, 'complicated.conf'))
100
+ end
101
+
102
+ describe "#get_repo" do
103
+ it 'should fetch a repo by a string containing the name' do
104
+ @config.get_repo('gitolite').should be_an_instance_of Gitolite::Config::Repo
105
+ end
106
+
107
+ it 'should fetch a repo via a symbol representing the name' do
108
+ @config.get_repo(:gitolite).should be_an_instance_of Gitolite::Config::Repo
109
+ end
110
+
111
+ it 'should return nil for a repo that does not exist' do
112
+ @config.get_repo(:glite).should be nil
113
+ end
114
+ end
115
+
116
+ describe "#has_repo?" do
117
+ it 'should return false for a repo that does not exist' do
118
+ @config.has_repo?(:glite).should be false
119
+ end
120
+
121
+ it 'should check for the existance of a repo given a repo object' do
122
+ r = @config.repos["gitolite"]
123
+ @config.has_repo?(r).should be true
124
+ end
125
+
126
+ it 'should check for the existance of a repo given a string containing the name' do
127
+ @config.has_repo?('gitolite').should be true
128
+ end
129
+
130
+ it 'should check for the existance of a repo given a symbol representing the name' do
131
+ @config.has_repo?(:gitolite).should be true
132
+ end
133
+ end
134
+
135
+ describe "#add_repo" do
136
+ it 'should throw an ArgumentError for non-Gitolite::Config::Repo objects passed in' do
137
+ lambda{ @config.add_repo("not-a-repo") }.should raise_error(ArgumentError)
138
+ end
139
+
140
+ it 'should add a given repo to the list of repos' do
141
+ r = Gitolite::Config::Repo.new('cool_repo')
142
+ nrepos = @config.repos.size
143
+ @config.add_repo(r)
144
+
145
+ @config.repos.size.should == nrepos + 1
146
+ @config.has_repo?(:cool_repo).should be true
147
+ end
148
+
149
+ it 'should merge a given repo with an existing repo' do
150
+ #Make two new repos
151
+ repo1 = Gitolite::Config::Repo.new('cool_repo')
152
+ repo2 = Gitolite::Config::Repo.new('cool_repo')
153
+
154
+ #Add some perms to those repos
155
+ repo1.add_permission("RW+", "", "bob", "joe", "sam")
156
+ repo1.add_permission("R", "", "sue", "jen", "greg")
157
+ repo1.add_permission("-", "refs/tags/test[0-9]", "@students", "jessica")
158
+ repo1.add_permission("RW", "refs/tags/test[0-9]", "@teachers", "bill", "todd")
159
+ repo1.add_permission("R", "refs/tags/test[0-9]", "@profs")
160
+
161
+ repo2.add_permission("RW+", "", "jim", "cynthia", "arnold")
162
+ repo2.add_permission("R", "", "daniel", "mary", "ben")
163
+ repo2.add_permission("-", "refs/tags/test[0-9]", "@more_students", "stephanie")
164
+ repo2.add_permission("RW", "refs/tags/test[0-9]", "@student_teachers", "mike", "judy")
165
+ repo2.add_permission("R", "refs/tags/test[0-9]", "@leaders")
166
+
167
+ #Add the repos
168
+ @config.add_repo(repo1)
169
+ @config.add_repo(repo2)
170
+
171
+ #Make sure perms were properly merged
172
+ end
173
+
174
+ it 'should overwrite an existing repo when overwrite = true' do
175
+ #Make two new repos
176
+ repo1 = Gitolite::Config::Repo.new('cool_repo')
177
+ repo2 = Gitolite::Config::Repo.new('cool_repo')
178
+
179
+ #Add some perms to those repos
180
+ repo1.add_permission("RW+", "", "bob", "joe", "sam")
181
+ repo1.add_permission("R", "", "sue", "jen", "greg")
182
+ repo2.add_permission("RW+", "", "jim", "cynthia", "arnold")
183
+ repo2.add_permission("R", "", "daniel", "mary", "ben")
184
+
185
+ #Add the repos
186
+ @config.add_repo(repo1)
187
+ @config.add_repo(repo2, true)
188
+
189
+ #Make sure repo2 overwrote repo1
190
+ end
191
+ end
192
+
193
+ describe "#rm_repo" do
194
+ it 'should remove a repo for the Gitolite::Config::Repo object given' do
195
+ r = @config.get_repo(:gitolite)
196
+ r2 = @config.rm_repo(r)
197
+ r2.name.should == r.name
198
+ r2.permissions.length.should == r.permissions.length
199
+ r2.owner.should == r.owner
200
+ r2.description.should == r.description
201
+ end
202
+
203
+ it 'should remove a repo given a string containing the name' do
204
+ r = @config.get_repo(:gitolite)
205
+ r2 = @config.rm_repo('gitolite')
206
+ r2.name.should == r.name
207
+ r2.permissions.length.should == r.permissions.length
208
+ r2.owner.should == r.owner
209
+ r2.description.should == r.description
210
+ end
211
+
212
+ it 'should remove a repo given a symbol representing the name' do
213
+ r = @config.get_repo(:gitolite)
214
+ r2 = @config.rm_repo(:gitolite)
215
+ r2.name.should == r.name
216
+ r2.permissions.length.should == r.permissions.length
217
+ r2.owner.should == r.owner
218
+ r2.description.should == r.description
219
+ end
220
+ end
221
+ end
222
+
223
+ describe "group management" do
224
+ before :each do
225
+ @config = Gitolite::Config.new(File.join(conf_dir, 'complicated.conf'))
226
+ end
227
+
228
+ describe "#has_group?" do
229
+ it 'should find the staff group using a symbol' do
230
+ @config.has_group?(:staff).should be true
231
+ end
232
+
233
+ it 'should find the staff group using a string' do
234
+ @config.has_group?('staff').should be true
235
+ end
236
+
237
+ it 'should find the staff group using a Gitolite::Config::Group object' do
238
+ g = Gitolite::Config::Group.new("staff")
239
+ @config.has_group?(g).should be true
240
+ end
241
+ end
242
+
243
+ describe "#get_group" do
244
+ it 'should return the Gitolite::Config::Group object for the group name String' do
245
+ g = @config.get_group("staff")
246
+ g.is_a?(Gitolite::Config::Group).should be true
247
+ g.size.should == 6
248
+ end
249
+
250
+ it 'should return the Gitolite::Config::Group object for the group name Symbol' do
251
+ g = @config.get_group(:staff)
252
+ g.is_a?(Gitolite::Config::Group).should be true
253
+ g.size.should == 6
254
+ end
255
+ end
256
+
257
+ describe "#add_group" do
258
+ it 'should throw an ArgumentError for non-Gitolite::Config::Group objects passed in' do
259
+ lambda{ @config.add_group("not-a-group") }.should raise_error(ArgumentError)
260
+ end
261
+
262
+ it 'should add a given group to the groups list' do
263
+ g = Gitolite::Config::Group.new('cool_group')
264
+ ngroups = @config.groups.size
265
+ @config.add_group(g)
266
+ @config.groups.size.should be ngroups + 1
267
+ @config.has_group?(:cool_group).should be true
268
+ end
269
+
270
+ end
271
+
272
+ describe "#rm_group" do
273
+ it 'should remove a group for the Gitolite::Config::Group object given' do
274
+ g = @config.get_group(:oss_repos)
275
+ g2 = @config.rm_group(g)
276
+ g.should_not be nil
277
+ g2.name.should == g.name
278
+ end
279
+
280
+ it 'should remove a group given a string containing the name' do
281
+ g = @config.get_group(:oss_repos)
282
+ g2 = @config.rm_group('oss_repos')
283
+ g2.name.should == g.name
284
+ end
285
+
286
+ it 'should remove a group given a symbol representing the name' do
287
+ g = @config.get_group(:oss_repos)
288
+ g2 = @config.rm_group(:oss_repos)
289
+ g2.name.should == g.name
290
+ end
291
+ end
292
+
293
+ end
294
+
295
+ describe "#to_file" do
296
+ it 'should create a file at the given path with the config\'s file name' do
297
+ c = Gitolite::Config.init
298
+ file = c.to_file('/tmp')
299
+ File.file?(File.join('/tmp', c.filename)).should be true
300
+ File.unlink(file)
301
+ end
302
+
303
+ it 'should create a file at the given path when a different filename is specified' do
304
+ filename = "test.conf"
305
+ c = Gitolite::Config.init
306
+ c.filename = filename
307
+ file = c.to_file('/tmp')
308
+ File.file?(File.join('/tmp', filename)).should be true
309
+ File.unlink(file)
310
+ end
311
+
312
+ it 'should raise an ArgumentError when an invalid path is specified' do
313
+ c = Gitolite::Config.init
314
+ lambda { c.to_file('/does/not/exist') }.should raise_error(ArgumentError)
315
+ end
316
+
317
+ it 'should raise an ArgumentError when a filename is specified in the path' do
318
+ c = Gitolite::Config.init
319
+ lambda{ c.to_file('/home/test.rb') }.should raise_error(ArgumentError)
320
+ end
321
+
322
+ it 'should resolve group dependencies such that all groups are defined before they are used' do
323
+ c = Gitolite::Config.init
324
+ c.filename = "test_deptree.conf"
325
+
326
+ # Build some groups out of order
327
+ g = Gitolite::Config::Group.new "groupa"
328
+ g.add_users "bob", "@groupb"
329
+ c.add_group(g)
330
+
331
+ g = Gitolite::Config::Group.new "groupb"
332
+ g.add_users "joe", "sam", "susan", "andrew"
333
+ c.add_group(g)
334
+
335
+ g = Gitolite::Config::Group.new "groupc"
336
+ g.add_users "jane", "@groupb", "brandon"
337
+ c.add_group(g)
338
+
339
+ g = Gitolite::Config::Group.new "groupd"
340
+ g.add_users "larry", "@groupc"
341
+ c.add_group(g)
342
+
343
+ # Write the config to a file
344
+ file = c.to_file('/tmp')
345
+
346
+ # Read the conf and make sure our order is correct
347
+ f = File.read(file)
348
+ lines = f.lines.map {|l| l.strip}
349
+
350
+ # Compare the file lines. Spacing is important here since we are doing a direct comparision
351
+ lines[0].should == "@groupb = andrew joe sam susan"
352
+ lines[1].should == "@groupc = @groupb brandon jane"
353
+ lines[2].should == "@groupd = @groupc larry"
354
+ lines[3].should == "@groupa = @groupb bob"
355
+
356
+ # Cleanup
357
+ File.unlink(file)
358
+ end
359
+
360
+ it 'should raise a GroupDependencyError if there is a cyclic dependency' do
361
+ c = Gitolite::Config.init
362
+ c.filename = "test_deptree.conf"
363
+
364
+ # Build some groups out of order
365
+ g = Gitolite::Config::Group.new "groupa"
366
+ g.add_users "bob", "@groupb"
367
+ c.add_group(g)
368
+
369
+ g = Gitolite::Config::Group.new "groupb"
370
+ g.add_users "joe", "sam", "susan", "@groupc"
371
+ c.add_group(g)
372
+
373
+ g = Gitolite::Config::Group.new "groupc"
374
+ g.add_users "jane", "@groupa", "brandon"
375
+ c.add_group(g)
376
+
377
+ g = Gitolite::Config::Group.new "groupd"
378
+ g.add_users "larry", "@groupc"
379
+ c.add_group(g)
380
+
381
+ # Attempt to write the config file
382
+ lambda{ c.to_file('/tmp')}.should raise_error(Gitolite::Config::GroupDependencyError)
383
+ end
384
+
385
+ it 'should resolve group dependencies even when there are disconnected portions of the graph' do
386
+ c = Gitolite::Config.init
387
+ c.filename = "test_deptree.conf"
388
+
389
+ # Build some groups out of order
390
+ g = Gitolite::Config::Group.new "groupa"
391
+ g.add_users "bob", "timmy", "stephanie"
392
+ c.add_group(g)
393
+
394
+ g = Gitolite::Config::Group.new "groupb"
395
+ g.add_users "joe", "sam", "susan", "andrew"
396
+ c.add_group(g)
397
+
398
+ g = Gitolite::Config::Group.new "groupc"
399
+ g.add_users "jane", "earl", "brandon", "@groupa"
400
+ c.add_group(g)
401
+
402
+ g = Gitolite::Config::Group.new "groupd"
403
+ g.add_users "larry", "chris", "emily"
404
+ c.add_group(g)
405
+
406
+ # Write the config to a file
407
+ file = c.to_file('/tmp')
408
+
409
+ # Read the conf and make sure our order is correct
410
+ f = File.read(file)
411
+ lines = f.lines.map {|l| l.strip}
412
+
413
+ # Compare the file lines. Spacing is important here since we are doing a direct comparision
414
+ lines[0].should == "@groupd = chris emily larry"
415
+ lines[1].should == "@groupb = andrew joe sam susan"
416
+ lines[2].should == "@groupa = bob stephanie timmy"
417
+ lines[3].should == "@groupc = @groupa brandon earl jane"
418
+
419
+ # Cleanup
420
+ File.unlink(file)
421
+ end
422
+ end
423
+
424
+ describe "#cleanup_config_line" do
425
+ before(:each) do
426
+ @config = Gitolite::Config.init
427
+ end
428
+
429
+ it 'should remove comments' do
430
+ s = "#comment"
431
+ @config.instance_eval { cleanup_config_line(s) }.empty?.should == true
432
+ end
433
+
434
+ it 'should remove inline comments, keeping content before the comment' do
435
+ s = "blablabla #comment"
436
+ @config.instance_eval { cleanup_config_line(s) }.should == "blablabla"
437
+ end
438
+
439
+ it 'should pad = with spaces on each side' do
440
+ s = "bob=joe"
441
+ @config.instance_eval { cleanup_config_line(s) }.should == "bob = joe"
442
+ end
443
+
444
+ it 'should replace multiple space characters with a single space' do
445
+ s = "bob = joe"
446
+ @config.instance_eval { cleanup_config_line(s) }.should == "bob = joe"
447
+ end
448
+
449
+ it 'should cleanup whitespace at the beginning and end of lines' do
450
+ s = " bob = joe "
451
+ @config.instance_eval { cleanup_config_line(s) }.should == "bob = joe"
452
+ end
453
+
454
+ it 'should cleanup whitespace and comments effectively' do
455
+ s = " bob = joe #comment"
456
+ @config.instance_eval { cleanup_config_line(s) }.should == "bob = joe"
457
+ end
458
+ end
459
+ end