gitolite-dtg 0.1.0

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