6 use Digest
::MD5
qw(md5);
25 require Digest
::SHA
::PurePerl
;
26 Digest
::SHA
::PurePerl
->import(
29 die "One of Digest::SHA or Digest::SHA1 or Digest::SHA::PurePerl "
30 . "must be available\n";
36 $self->{uuid
} = '' unless $self->{uuid
};
40 @md5 = unpack('C*', md5
(time . $$ . rand() . join(':',%$self)));
42 $md5[6] = 0x40 | ($md5[6] & 0x0F); # Version 4 -- random
43 $md5[8] = 0x80 | ($md5[8] & 0x3F); # RFC 4122 specification
45 '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x',
49 sub _remove_ssh_leftovers
{
52 system('rm', '-f', "$Girocco::Config::chroot/etc/sshkeys/$self->{name}");
53 @files = glob("'$Girocco::Config::chroot/etc/sshcerts/${Girocco::Config::nickname}_$self->{name}'_user_*.pem");
54 system('rm', '-f', @files) if @files;
55 system('rm', '-f', "$Girocco::Config::chroot/etc/sshactive/$self->{name}");
56 @files = glob("'$Girocco::Config::chroot/etc/sshactive/$self->{name}',*");
57 system('rm', '-f', @files) if @files;
61 use POSIX
qw(strftime);
63 my (undef, undef, $gid) = getgrnam($Girocco::Config
::owning_group
||'');
64 my $owngroupid = $gid ?
$gid : 65534;
65 Girocco
::User
->load($self->{name
}) and die "User $self->{name} already exists";
66 $self->{uuid
} = $self->_gen_uuid;
67 my ($S,$M,$H,$d,$m,$y) = gmtime(time());
68 $self->{creationtime
} = strftime
("%Y%m%d_%H%M%S", $S, $M, $H, $d, $m, $y, -1, -1, -1);
69 my $email_uuid_etc = join ',', $self->{email
}, $self->{uuid
}, $self->{creationtime
};
70 filedb_atomic_append
(jailed_file
('/etc/passwd'),
71 join(':', $self->{name
}, 'x', '\i', $owngroupid, $email_uuid_etc, '/', '/bin/git-shell-verify'),
73 $self->_remove_ssh_leftovers;
78 filedb_atomic_edit
(jailed_file
('/etc/passwd'),
82 if ($self->{name
} eq (split /:/)[0]) {
83 # preserve all but login name and first 2 fields of comment field
84 # creating a uuid (field 2 of comment field) if one is not already present
85 my @fields=split(/:/, $_, -1);
86 $fields[0] = $self->{name
};
87 my @subfields = split(',', $fields[4]||'', -1);
88 $self->{uuid
} = $subfields[1] || '';
89 $self->{uuid
} or $self->{uuid
} = $self->_gen_uuid;
90 $subfields[0] = $self->{email
};
91 $subfields[1] = $self->{uuid
};
92 $fields[4] = join(',', @subfields);
93 return join(':', @fields)."\n";
104 $self->_remove_ssh_leftovers;
105 filedb_atomic_edit
(jailed_file
('/etc/passwd'),
107 $self->{name
} ne (split /:/)[0] and return $_;
115 '/etc/sshkeys/'.$self->{name
};
120 open my $fd, '<', jailed_file
($self->_sshkey_path) or die "sshkey load failed: $!";
126 if (/^(?:no-pty )?(ssh-(?:dss|rsa) .*)$/) {
128 } elsif (/^# ([A-Z]+)AUTH ([0-9a-f]+) (\d+)/) {
130 $auth = $2 unless (time >= $expire);
131 $authtype = $1 if $auth;
135 my $keys = join("\n", @keys); chomp $keys;
136 ($keys, $auth, $authtype);
142 foreach (split /\r\n|\r|\n/, $keys) {
143 next if /^[ \t]*$/ || /^[ \t]*#/;
146 return join("\n", @lines);
151 $self->{keys} = _trimkeys
($self->{keys} || '');
152 open my $fd, '>', jailed_file
($self->_sshkey_path) or die "sshkey save failed: $!";
153 if (defined($self->{auth
}) && $self->{auth
}) {
154 my $expire = time + 24 * 3600;
155 my $typestr = $self->{authtype
} ?
uc($self->{authtype
}) : 'REPO';
156 print $fd "# ${typestr}AUTH $self->{auth} $expire\n";
158 print $fd map("no-pty $_\n", split(/\n/, $self->{keys}));
160 chmod 0664, jailed_file
($self->_sshkey_path);
163 # private constructor, do not use
166 my ($name, $rsrv_sfx_ok) = @_;
167 Girocco
::User
::valid_name
($name, $rsrv_sfx_ok ?
$Girocco::Config
::chroot."/etc/sshkeys" : undef)
168 or die "refusing to create user with invalid name ($name)!";
169 my $proj = { name
=> $name };
174 # public constructor #0
175 # creates a virtual user not connected to disk record
176 # you can conjure() it later to disk
180 my $self = $class->_new($name);
184 # public constructor #1
189 open my $fd, '<', jailed_file
("/etc/passwd") or die "user load failed: $!";
190 my $r = qr/^\Q$name\E:/;
191 foreach (grep /$r/, <$fd>) {
194 my (undef, undef, $uid, undef, $email_uuid_etc) = split /:/;
195 my $self = $class->_new($name, 1);
197 ($self->{keys}, $self->{auth
}, $self->{authtype
}) = $self->_sshkey_load;
198 ($self->{email
}, $self->{uuid
}, $self->{creationtime
}) = split ',', $email_uuid_etc;
201 $self->{uuid
} or !valid_email
($self->{email
}) or $self->_passwd_update;
208 # public constructor #2
213 open my $fd, '<', jailed_file
("/etc/passwd") or die "user load failed: $!";
214 my $r = qr/^[^:]+:[^:]*:\Q$uid\E:/;
215 foreach (grep /$r/, <$fd>) {
218 my ($name, undef, undef, undef, $email_uuid_etc) = split /:/;
219 my $self = $class->_new($name, 1);
221 ($self->{keys}, $self->{auth
}, $self->{authtype
}) = $self->_sshkey_load;
222 ($self->{email
}, $self->{uuid
}, $self->{creationtime
}) = split ',', $email_uuid_etc;
225 $self->{uuid
} or $self->_passwd_update;
232 # $user may not be in sane state if this returns false!
236 my $cgi = $gcgi->cgi;
238 $self->{name
} = $gcgi->wparam('name');
239 Girocco
::User
::valid_name
($self->{name
})
240 or $gcgi->err("Name contains invalid characters.");
242 length($self->{name
}) <= 64
243 or $gcgi->err("Your user name is longer than 64 characters. Do you really need that much?");
245 $self->{email
} = $gcgi->wparam('email');
246 valid_email
($self->{email
})
247 or $gcgi->err("Your email sure looks weird...?");
248 length($self->{email
}) <= 96
249 or $gcgi->err("Your email is longer than 96 characters. Do you really need that much?");
251 $self->keys_fill($gcgi);
257 my $email = shift || '';
259 if (valid_email
($email)) {
260 $self->{email
} = $email;
261 $self->_passwd_update;
263 $gcgi->err("Your email sure looks weird...?");
266 not $gcgi->err_check;
272 my $cgi = $gcgi->cgi;
274 $self->{keys} = _trimkeys
($cgi->param('keys'));
275 length($self->{keys}) <= 4096
276 or $gcgi->err("The list of keys is more than 4kb. Do you really need that much?");
277 foreach my $key (split /\r?\n/, $self->{keys}) {
278 my ($type, $bits, $fingerprint, $comment);
279 ($type, $bits, $fingerprint, $comment) = sshpub_validate
($key)
280 if $key =~ /^ssh-(?:dss|rsa) [0-9A-Za-z+\/=]+ \S
+$/;
282 my $keyval = CGI
::escapeHTML
($key);
284 $dsablurb = ' or ssh-dss' unless $Girocco::Config
::disable_dsa
;
286 Your ssh key ("$keyval") appears to have an invalid format
287 (does not start with ssh-rsa$dsablurb or does not end with a whitespace-free comment) -
288 maybe your browser has split a single key onto multiple lines?
290 } elsif ($Girocco::Config
::disable_dsa
&& $type eq 'ssh-dss') {
291 my $keyval = CGI
::escapeHTML
($key);
293 Your ssh key ("$keyval") appears to be of type dsa but only rsa keys are
294 supported - please generate an rsa key (starts with ssh-rsa) and try again
296 } elsif ($Girocco::Config
::min_key_length
&& $bits < $Girocco::Config
::min_key_length
) {
297 my $keyval = CGI
::escapeHTML
($key);
299 Your ssh key ("$keyval") appears to have only $bits bit(s) but at least
300 $Girocco::Config::min_key_length are required - please generate a longer key
305 not $gcgi->err_check;
316 my @keys = split(/\r?\n/, $self->{keys});
319 my %types = ('ssh-dss' => 'DSA', 'ssh-rsa' => 'RSA');
323 my ($type, $bits, $fingerprint, $comment) = sshpub_validate
($_);
324 next unless $type && $types{$type};
325 my $euser = CGI
::escapeHTML
(CGI
::Util
::escape
($self->{name
}));
326 $html .= "<li>$bits <tt>$fingerprint</tt> ($types{$type}) $comment";
327 $html .= "<br /><a target=\"_blank\" ".
328 "href=\"@{[url_path($Girocco::Config::webadmurl)]}/usercert.cgi/$euser/$line/".
329 $Girocco::Config
::nickname
."_${euser}_user_$line.pem\">".
330 "download https push user authentication certificate</a> <sup class=\"sup\"><span>".
331 "<a target=\"_blank\" href=\"@{[url_path($Girocco::Config::htmlurl)]}/httpspush.html\">".
332 "(learn more)</a></span></sup>"
333 if $type eq 'ssh-rsa' && $Girocco::Config
::httpspushurl
&&
334 $Girocco::Config
::clientcert
&&
335 $Girocco::Config
::clientkey
;
345 $type = 'REPO' unless $type && $type =~ /^[A-Z]+$/;
347 $self->{authtype
} = $type;
348 $self->{auth
} = sha1_hex
(time . $$ . rand() . $self->{keys});
356 delete $self->{auth
};
357 delete $self->{authtype
};
363 return @
{$self->{projects
}} if defined($self->{projects
});
364 my @projects = filedb_atomic_grep
(jailed_file
('/etc/group'),
368 my ($group, $users) = (split /:/)[0,3];
369 $group if $users && $users =~ /(^|,)\Q$self->{name}\E(,|$)/;
372 $self->{projects
} = \
@projects;
373 @
{$self->{projects
}};
386 require Girocco
::Project
;
387 foreach ($self->get_projects) {
388 if (Girocco
::Project
::does_exist
($_, 1)) {
389 my $project = Girocco
::Project
->load($_);
390 $project->update if $project->remove_user($self->{name
});
394 $self->_passwd_remove;
399 # Note that 'mob' and 'everyone' are NOT reserved names per se, but they are
400 # names with special semantics and they should be allowed in project membership
401 # lists so they are therefore NOT included in the reservedusernames list.
402 # 'git', 'lock' and 'bundle' are reserved so that the personal mob names 'mob.git',
403 # 'mob.lock' or 'mob.bundle' are not needed as they would be invalid.
404 our %reservedusernames = (
409 lc($Girocco::Config
::cgi_user
) => 1,
410 lc($Girocco::Config
::mirror_user
) => 1,
418 /^[a-zA-Z0-9][a-zA-Z0-9+._-]*$/
422 and (not m
#\.lock$#i)
423 and (not m
#\.bundle$#i)
424 and (not m
#^mob[._]#i)
425 and !exists($reservedusernames{lc($_)})
426 and !has_reserved_suffix
($_, $_[1]);
430 my ($name, $nodie) = @_;
431 if (!Girocco
::User
::valid_name
($name, $Girocco::Config
::chroot."/etc/sshkeys")) {
432 die "tried to query for user with invalid name $name!" unless $nodie;
435 (-e jailed_file
("/etc/sshkeys/$name"));
440 $Girocco::Config
::chrooted
and undef; # TODO for ACLs within chroot
441 scalar(getpwnam($name));