1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
|
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;
# === add-auth-keys ===
# part of the gitolite (GL) suite
# (1) - "compiles" ~/.ssh/authorized_keys from the list of pub-keys
# (2) - also "compiles" the user-friendly GL conf file into something easier
# to parse. We're doing this because both the gl-auth-command and the
# (gl-)update hook need this, and it seems easier to do this than
# replicate the parsing code in both those places. As a bonus, it's
# probably more efficient.
# (3) - finally does what I have resisted doing all along -- handle gitweb and
# git-daemon access. It won't *setup* gitweb/daemon for you -- you have
# to that yourself. What this does is make sure that "repo.git"
# contains the file "git-daemon-export-ok" (for daemon case) and the
# line "repo.git" exists in the "projects.list" file (for gitweb case).
# how run: manual, by GL admin
# when:
# - anytime a pubkey is added/deleted
# - anytime gitolite.conf is changed
# input:
# - GL_CONF (default: ~/.gitolite/conf/gitolite.conf)
# - GL_KEYDIR (default: ~/.gitolite/keydir)
# output:
# - ~/.ssh/authorized_keys (dictated by sshd)
# - GL_CONF_COMPILED (default: ~/.gitolite/conf/gitolite.conf-compiled.pm)
# security:
# - touches a very critical system file that manages the restrictions on
# incoming users. Be sure to audit AUTH_COMMAND and AUTH_OPTIONS (see
# below) on any change to this script
# - no security checks within program. The GL admin runs this manually
# warnings:
# - if the "start" line exists, but the "end" line does not, you lose the
# rest of the existing authkey file. In general, "don't do that (TM)",
# but we do have a "vim -d" popping up so you can see the changes being
# made, just in case...
# ----------------------------------------------------------------------------
# common definitions
# ----------------------------------------------------------------------------
# setup quiet mode if asked; please do not use this when running manually
open STDOUT, ">", "/dev/null" if (@ARGV and shift eq '-q');
# these are set by the "rc" file
our ($GL_ADMINDIR, $GL_CONF, $GL_KEYDIR, $GL_CONF_COMPILED, $REPO_BASE, $REPO_UMASK, $PROJECTS_LIST, $GIT_PATH, $GL_WILDREPOS, $GL_GITCONFIG_KEYS, $GL_PACKAGE_HOOKS, $GL_BIG_CONFIG, $GL_NO_DAEMON_NO_GITWEB);
# and these are set by gitolite.pm
our ($REPONAME_PATT, $REPOPATT_PATT, $USERNAME_PATT, $AUTH_COMMAND, $AUTH_OPTIONS, $ABRT, $WARN);
# the common setup module is in the same directory as this running program is
my $bindir = $0;
$bindir =~ s/\/[^\/]+$//;
$bindir = "$ENV{PWD}/$bindir" unless $bindir =~ /^\//;
require "$bindir/gitolite.pm";
# ask where the rc file is, get it, and "do" it
&where_is_rc();
die "$ABRT parse $ENV{GL_RC} failed: " . ($! or $@) unless do $ENV{GL_RC};
# add a custom path for git binaries, if specified
$ENV{PATH} .= ":$GIT_PATH" if $GIT_PATH;
# ----------------------------------------------------------------------------
# definitions specific to this program
# ----------------------------------------------------------------------------
# command and options for authorized_keys
$AUTH_COMMAND="$bindir/gl-auth-command";
$AUTH_OPTIONS="no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty";
# groups can now represent user groups or repo groups.
# $groups{group}{member} = "master" (or name of fragment file in which the
# group is defined).
our %groups = ();
# %repos has two functions.
# $repos{repo}{R|W}{user} = 1 if user has R (or W) permissions for at least
# one branch in repo. This is used by the "level 1 check" (see faq). There's
# also the new "C" (create a repo) permission now
# $repos{repo}{user} is a list of {ref, perms} pairs. This is used by the
# level 2 check. In order to allow "exclude" rules, the order of rules now
# matters, so what used to be entirely "hash of hash of hash" now has a list
# in between :)
my %repos = ();
# rule sequence number
my $rule_seq = 0;
# <sigh>... having been forced to use a list as described above, we lose some
# efficiency due to the possibility of the same {ref, perms} pair showing up
# multiple times for the same repo+user. So...
my %rurp_seen = ();
our $current_data_version; # this comes from gitolite.pm
# catch usernames<->pubkeys mismatches; search for "lint" below
my %user_list = ();
# repo configurations
my %repo_config = ();
# gitweb descriptions and owners; plain text, keyed by "$repo.git"
my %desc = ();
my %owner = ();
# set the umask before creating any files
umask($REPO_UMASK);
# ----------------------------------------------------------------------------
# subroutines
# ----------------------------------------------------------------------------
sub expand_list
{
my @list = @_;
my @new_list = ();
for my $item (@list)
{
if ($item =~ /^@/) # nested group
{
die "$ABRT undefined group $item\n" unless $groups{$item};
# add those names to the list
push @new_list, sort keys %{ $groups{$item} };
}
else
{
push @new_list, $item;
}
}
return @new_list;
}
# ----------------------------------------------------------------------------
# "compile" GL conf
# ----------------------------------------------------------------------------
sub parse_conf_file
{
my ($conffile, $fragment) = @_;
# the second arg, $fragment, is passed in as "master" when parsing the
# main config, and the fragment name when parsing a fragment. In the
# latter case, the parser uses that information to ignore (and warn about)
# any repos in the fragment that are not members of the "repo group" of
# the same name.
my %ignored = ();
my $conf_fh = wrap_open( "<", $conffile );
# the syntax is fairly simple, so we parse it inline
my @repos;
while (<$conf_fh>)
{
# kill comments, but take care of "#" inside *simple* strings
s/^((".*?"|[^#"])*)#.*/$1/;
# normalise whitespace; keeps later regexes very simple
s/=/ = /;
s/\s+/ /g;
s/^ //;
s/ $//;
# and blank lines
next unless /\S/;
# user or repo groups
if (/^(@\S+) = (.*)/)
{
die "$ABRT defining groups is not allowed inside fragments\n"
if $GL_BIG_CONFIG and $fragment ne 'master';
# store the members of each group as hash key. Keep track of when
# the group was *first* created by using $fragment as the *value*
do { $groups{$1}{$_} ||= $fragment } for ( expand_list( split(' ', $2) ) );
die "$ABRT bad group $1\n" unless $1 =~ $REPONAME_PATT;
}
# repo(s)
elsif (/^repo (.*)/)
{
# grab the list...
@repos = split ' ', $1;
unless (@repos == 1 and $repos[0] eq '@all') {
# ...expand groups in the default case
@repos = expand_list ( @repos ) unless $GL_BIG_CONFIG;
# ...sanity check
for (@repos) {
die "$ABRT bad reponame $_\n"
if ($GL_WILDREPOS and $_ !~ $REPOPATT_PATT);
die "$ABRT bad reponame $_ or you forgot to set \$GL_WILDREPOS\n"
if (not $GL_WILDREPOS and $_ !~ $REPONAME_PATT);
}
}
s/\bCREAT[EO]R\b/\$creator/g for @repos;
}
# actual permission line
elsif (/^(-|C|R|RW\+?D?) (.* )?= (.+)/)
{
my $perms = $1;
my @refs; @refs = split(' ', $2) if $2;
@refs = expand_list ( @refs );
my @users = split ' ', $3;
die "$ABRT \$GL_WILDREPOS is not set, you cant use 'C' in config\n" if $perms eq 'C' and not $GL_WILDREPOS;
# if no ref is given, this PERM applies to all refs
@refs = qw(refs/.*) unless @refs;
# deprecation warning
map { warn "WARNING: old syntax 'PATH/' found; please use new syntax 'NAME/'\n" if s(^PATH/)(NAME/) } @refs;
# fully qualify refs that dont start with "refs/" or "NAME/";
# prefix them with "refs/heads/"
@refs = map { m(^(refs|NAME)/) or s(^)(refs/heads/); $_ } @refs;
@refs = map { s(/USER/)(/\$gl_user/); $_ } @refs;
# expand the user list, unless it is just "@all"
@users = expand_list ( @users )
unless ($GL_BIG_CONFIG or (@users == 1 and $users[0] eq '@all'));
do { die "$ABRT bad username $_\n" unless $_ =~ $USERNAME_PATT } for @users;
s/\bCREAT[EO]R\b/~\$creator/g for @users;
s/\bREADERS\b/\$readers/g for @users;
s/\bWRITERS\b/\$writers/g for @users;
# ok, we can finally populate the %repos hash
for my $repo (@repos) # each repo in the current stanza
{
# if we're processing a delegated config file (not the master
# config), we need to prevent attempts by that admin to obtain
# rights on stuff outside his domain
# trying to set access for $repo (='foo')...
if (
# processing the master config, not a fragment
( $fragment eq 'master' ) or
# fragment is also called 'foo' (you're allowed to have a
# fragment that is only concerned with one repo)
( $fragment eq $repo ) or
# same thing in big-config-land; foo is just @foo now
( $GL_BIG_CONFIG and "\@$fragment" eq $repo ) or
# fragment is called "bar" and "@bar = foo" has been
# defined in the master config
( ($groups{"\@$fragment"}{$repo} || '') eq 'master' )
) {
# all these are fine
} else {
# this is a little more complex
# fragment is called "bar", one or more "@bar = regex"
# have been specified in master, and "foo" matches some
# such "regex"
my @matched = grep { $repo =~ /^$_$/ }
grep { $groups{"\@$fragment"}{$_} eq 'master' }
sort keys %{ $groups{"\@$fragment"} };
if (@matched < 1) {
$ignored{$fragment}{$repo} = 1;
next;
}
}
for my $user (@users)
{
$user_list{$user}++; # only to catch lint, see later
# for 1st level check (see faq/tips doc)
$repos{$repo}{C}{$user} = 1, next if $perms eq 'C';
$repos{$repo}{R}{$user} = 1 if $perms =~ /R/;
$repos{$repo}{W}{$user} = 1 if $perms =~ /W|D/;
# if the user specified even a single 'D' anywhere, make
# that fact easy to find; this changes the meaning of RW+
# to no longer permit deletes (see update hook)
$repos{$repo}{DELETE_IS_D} = 1 if $perms =~ /D/;
# for 2nd level check, store each "ref, perms" pair in order
for my $ref (@refs)
{
# checking NAME based restrictions is expensive for
# the update hook (see the changes to src/hooks/update
# in this commit for why) so we would *very* much like
# to avoid doing it for the large majority of repos
# that do *not* use NAME limits. Setting a flag that
# can be checked right away will help us do that
$repos{$repo}{NAME_LIMITS} = 1 if $ref =~ /^NAME\//;
my $p_user = $user; $p_user =~ s/(creator|readers|writers)$/$1 - wild/;
push @{ $repos{$repo}{$p_user} }, [ $rule_seq++, $ref, $perms ]
unless $rurp_seen{$repo}{$p_user}{$ref}{$perms}++;
}
}
}
}
# configuration
elsif (/^config (.+) = ?(.*)/)
{
my ($key, $value) = ($1, $2);
my @validkeys = split (' ', ($GL_GITCONFIG_KEYS || ''));
my @matched = grep { $key =~ /^$_$/ } @validkeys;
die "$ABRT git config $key not allowed\n" if (@matched < 1);
for my $repo (@repos) # each repo in the current stanza
{
$repo_config{$repo}{$key} = $value;
}
}
# include
elsif (/^include "(.+)"/)
{
my $file = $1;
$file = "$GL_ADMINDIR/conf/$file" unless $file =~ /^\//;
die "$WARN $fragment attempting to include configuration\n" if $fragment ne 'master';
die "$ABRT included file not found: '$file'\n" unless -f $file;
parse_conf_file($file, $fragment);
}
# very simple syntax for the gitweb description of repo; one of:
# reponame = "some description string"
# reponame "owner name" = "some description string"
elsif (/^(\S+)(?: "(.*?)")? = "(.*)"$/)
{
my ($repo, $owner, $desc) = ($1, $2, $3);
die "$ABRT bad repo name $repo\n" unless $repo =~ $REPONAME_PATT;
die "$WARN $fragment attempting to set description for $repo\n" if
$fragment ne 'master' and $fragment ne $repo and ($groups{"\@$fragment"}{$repo} || '') ne 'master';
$desc{"$repo.git"} = $desc;
$owner{"$repo.git"} = $owner || '';
}
else
{
die "$ABRT can't make head or tail of '$_'\n";
}
}
for my $ig (sort keys %ignored)
{
warn "\n\t\t***** WARNING *****\n" .
"\t$ig.conf attempting to set access for " .
join (", ", sort keys %{ $ignored{$ig} }) . "\n";
}
}
# parse the main config file
parse_conf_file($GL_CONF, 'master');
# parse any delegated fragments
wrap_chdir($GL_ADMINDIR);
for my $fragment_file (glob("conf/fragments/*.conf"))
{
# we already check (elsewhere) that a fragment called "foo" will not try
# to specify access control for a repo whose name is not "foo" or is not
# part of a group called "foo" created by master
# meanwhile, I found a possible attack where the admin for group B creates
# a "convenience" group of (a subset of) his users, and then the admin for
# repo group A (alphabetically before B) adds himself to that same group
# in his own fragment.
# as a result, admin_A now has access to group B repos :(
# so now we lock the groups hash to the value it had after parsing
# "master", and localise any changes to it by this fragment so that they
# don't propagate to the next fragment. Thus, each fragment now has only
# those groups that are defined in "master" and itself
local %groups = %groups;
my $fragment = $fragment_file;
$fragment =~ s/^conf\/fragments\/(.*).conf$/$1/;
parse_conf_file($fragment_file, $fragment);
}
my $compiled_fh = wrap_open( ">", $GL_CONF_COMPILED );
my $data_version = $current_data_version;
print $compiled_fh Data::Dumper->Dump([$data_version], [qw(*data_version)]);
my $dumped_data = Data::Dumper->Dump([\%repos], [qw(*repos)]);
# the dump uses single quotes, but we convert any strings containing $creator,
# $readers, $writers, to double quoted strings. A wee bit sneaky, but not too
# much...
$dumped_data =~ s/'(?=[^']*\$(?:creator|readers|writers|gl_user))~?(.*?)'/"$1"/g;
print $compiled_fh $dumped_data;
print $compiled_fh Data::Dumper->Dump([\%groups], [qw(*groups)]) if $GL_BIG_CONFIG and %groups;
close $compiled_fh or die "$ABRT close compiled-conf failed: $!\n";
# ----------------------------------------------------------------------------
# any new repos to be created?
# ----------------------------------------------------------------------------
# modern gits allow cloning from an empty repo, so we just create it
# but it turns out not everyone has "modern" gits :)
my $git_version = `git --version`;
die "
*** ERROR ***
did not get a proper version number. Please see if git is in the PATH on
the server. If it is not, please edit ~/.gitolite.rc on the server and
set the \$GIT_PATH variable to the correct value\n
" unless $git_version;
my ($gv_maj, $gv_min, $gv_patchrel) = ($git_version =~ m/git version (\d+)\.(\d+)\.(\d+)/);
die "$ABRT I can't understand $git_version\n" unless ($gv_maj >= 1);
$git_version = $gv_maj*10000 + $gv_min*100 + $gv_patchrel; # now it's "normalised"
die "\n\t\t***** AAARGH! *****\n" .
"\tyour git version is older than 1.6.2\n" .
"\tsince that is now more than one year old, and gitolite needs some of\n" .
"\tthe newer features, please upgrade.\n"
if $git_version < 10602; # that's 1.6.2 to you
# repo-base needs to be an absolute path for this loop to work right
# so if it was not already absolute, prefix $HOME.
my $repo_base_abs = ( $REPO_BASE =~ m(^/) ? $REPO_BASE : "$ENV{HOME}/$REPO_BASE" );
{
wrap_chdir("$repo_base_abs");
# autocreate repos. Start with the ones that are normal repos in %repos
my @repos = grep { $_ =~ $REPONAME_PATT and not /^@/ } sort keys %repos;
# then, for each repogroup, find the members of the group and add them in
map { push @repos, keys %{ $groups{$_} } } grep { /^@/ } keys %repos;
# weed out duplicates (the code in the loop below is disk activity!)
my %seen = map { $_ => 1 } @repos;
@repos = sort keys %seen;
for my $repo (sort @repos) {
next unless $repo =~ $REPONAME_PATT;
next if $repo =~ m(^\@|EXTCMD/); # these are not real repos
unless (-d "$repo.git") {
print STDERR "creating $repo...\n";
new_repo($repo, "$GL_ADMINDIR/hooks/common");
# new_repo would have chdir'd us away; come back
wrap_chdir("$repo_base_abs");
}
# when repos are copied over from elsewhere, one had to run easy install
# once again to make the new (OS-copied) repo contain the proper update
# hook. Perhaps we can make this easier now, and eliminate the easy
# install, with a quick check (and a new, empty, "hook" as a sentinel)
unless (-l "$repo.git/hooks/gitolite-hooked") {
ln_sf("$GL_ADMINDIR/hooks/common", "*", "$repo.git/hooks");
# in case of package install, GL_ADMINDIR is no longer the top cop;
# override with the package hooks
ln_sf("$GL_PACKAGE_HOOKS/common", "*", "$repo.git/hooks") if $GL_PACKAGE_HOOKS;
}
}
}
# ----------------------------------------------------------------------------
# update repo configurations
# ----------------------------------------------------------------------------
for my $repo (keys %repo_config) {
wrap_chdir("$repo_base_abs/$repo.git");
while ( my ($key, $value) = each(%{ $repo_config{$repo} }) ) {
if ($value) {
$value =~ s/^"(.*)"$/$1/;
system("git", "config", $key, $value);
} else {
system("git", "config", "--unset-all", $key);
}
}
}
# ----------------------------------------------------------------------------
# handle gitweb and daemon
# ----------------------------------------------------------------------------
# I just assume you'll never have any *real* users called "gitweb" or "daemon"
# :-) These are now "pseduo users" -- giving them "R" access to a repo is all
# you have to do
wrap_chdir("$repo_base_abs");
unless ($GL_NO_DAEMON_NO_GITWEB) {
# daemons first...
for my $repo (sort keys %repos) {
next unless $repo =~ $REPONAME_PATT;
next if $repo =~ m(^\@|EXTCMD/); # these are not real repos
my $export_ok = "$repo.git/git-daemon-export-ok";
if ($repos{$repo}{'R'}{'daemon'}) {
system("touch $export_ok");
} else {
unlink($export_ok);
}
}
my %projlist = ();
# ...then gitwebs
for my $repo (sort keys %repos) {
next unless $repo =~ $REPONAME_PATT;
next if $repo =~ m(^\@|EXTCMD/); # these are not real repos
my $desc_file = "$repo.git/description";
# note: having a description also counts as enabling gitweb
if ($repos{$repo}{'R'}{'gitweb'} or $desc{"$repo.git"}) {
$projlist{"$repo.git"} = 1;
# add the description file; no messages to user or error checking :)
$desc{"$repo.git"} and open(DESC, ">", $desc_file) and print DESC $desc{"$repo.git"} . "\n" and close DESC;
if ($owner{"$repo.git"}) {
# set the repository owner
system("git", "--git-dir=$repo.git", "config", "gitweb.owner", $owner{"$repo.git"});
} else {
# remove the repository owner setting
system("git --git-dir=$repo.git config --unset-all gitweb.owner 2>/dev/null");
}
} else {
# delete the description file; no messages to user or error checking :)
unlink $desc_file;
# remove the repository owner setting
system("git --git-dir=$repo.git config --unset-all gitweb.owner 2>/dev/null");
}
# unless there are other gitweb.* keys set, remove the section to keep the
# config file clean
my $keys = `git --git-dir=$repo.git config --get-regexp '^gitweb\\.' 2>/dev/null`;
if (length($keys) == 0) {
system("git --git-dir=$repo.git config --remove-section gitweb 2>/dev/null");
}
}
# update the project list
my $projlist_fh = wrap_open( ">", $PROJECTS_LIST);
for my $proj (sort keys %projlist) {
print $projlist_fh "$proj\n";
}
close $projlist_fh;
}
# ----------------------------------------------------------------------------
# "compile" ssh authorized_keys
# ----------------------------------------------------------------------------
my $authkeys_fh = wrap_open( "<", $ENV{HOME} . "/.ssh/authorized_keys",
"\tFor security reasons, gitolite will not *create* this file if it does\n" .
"\tnot already exist. Please see the \"admin\" document for details\n");
my $newkeys_fh = wrap_open( ">", $ENV{HOME} . "/.ssh/new_authkeys" );
# save existing authkeys minus the GL-added stuff
while (<$authkeys_fh>)
{
print $newkeys_fh $_ unless (/^# gito(sis-)?lite start/../^# gito(sis-)?lite end/);
}
# add our "start" line, each key on its own line (prefixed by command and
# options, in the standard ssh authorized_keys format), then the "end" line.
print $newkeys_fh "# gitolite start\n";
wrap_chdir($GL_KEYDIR);
for my $pubkey (`find . -type f`)
{
chomp($pubkey); $pubkey =~ s(^\./)();
# security check (thanks to divVerent for catching this)
unless ($pubkey =~ $REPONAME_PATT) {
print STDERR "$pubkey contains some unsavoury characters; ignored...\n";
next;
}
# lint check 1
unless ($pubkey =~ /\.pub$/)
{
print STDERR "WARNING: pubkey files should end with \".pub\", ignoring $pubkey\n";
next;
}
my $user = $pubkey;
$user =~ s(.*/)(); # foo/bar/baz.pub -> baz.pub
$user =~ s/(\@[^.]+)?\.pub$//; # baz.pub, baz@home.pub -> baz
# lint check 2
print STDERR "WARNING: pubkey $pubkey exists but user $user not in config\n"
unless $user_list{$user};
$user_list{$user} = 'has pubkey';
# apparently some pubkeys don't end in a newline...
my $pubkey_content = `cat $pubkey`;
$pubkey_content =~ s/\s*$/\n/;
# don't trust files with multiple lines (i.e., something after a newline)
if ($pubkey_content =~ /\n./)
{
print STDERR "WARNING: a pubkey file can only have one line (key); ignoring $pubkey\n";
next;
}
print $newkeys_fh "command=\"$AUTH_COMMAND $user\",$AUTH_OPTIONS ";
print $newkeys_fh $pubkey_content;
}
# lint check 3; a little more severe than the first two I guess...
{
my @no_pubkey =
grep { $_ !~ /^(gitweb|daemon|\@.*|~\$creator|\$readers|\$writers)$/ }
grep { $user_list{$_} ne 'has pubkey' }
keys %user_list;
if (@no_pubkey > 10) {
print STDERR "$WARN You have " . scalar(@no_pubkey) . " users WITHOUT pubkeys...!\n";
} elsif (@no_pubkey) {
print STDERR "$WARN the following users have no pubkeys:\n", join(",", sort @no_pubkey), "\n";
}
}
print $newkeys_fh "# gitolite end\n";
close $newkeys_fh or die "$ABRT close newkeys failed: $!\n";
# all done; overwrite the file (use cat to avoid perm changes)
system("cat $ENV{HOME}/.ssh/authorized_keys > $ENV{HOME}/.ssh/old_authkeys");
system("cat $ENV{HOME}/.ssh/new_authkeys > $ENV{HOME}/.ssh/authorized_keys")
and die "couldn't write authkeys file\n";
system("rm $ENV{HOME}/.ssh/new_authkeys");
|