diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 09:55:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 09:55:51 +0000 |
commit | 7685305e1f82212323ec32a321b1f5c623751b6c (patch) | |
tree | a1af617672e26aee4c1031a3aa83e8ff08f6a0a5 /contrib/commands | |
parent | Initial commit. (diff) | |
download | gitolite3-upstream.tar.xz gitolite3-upstream.zip |
Adding upstream version 3.6.12.upstream/3.6.12upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'contrib/commands')
-rwxr-xr-x | contrib/commands/compile-1 | 139 | ||||
-rwxr-xr-x | contrib/commands/ukm | 732 |
2 files changed, 871 insertions, 0 deletions
diff --git a/contrib/commands/compile-1 b/contrib/commands/compile-1 new file mode 100755 index 0000000..a5b5356 --- /dev/null +++ b/contrib/commands/compile-1 @@ -0,0 +1,139 @@ +#!/usr/bin/perl -s +use strict; +use warnings; + +# DESCRIPTION: + +# This program is meant to re-compile the access rules (and 'config' or +# 'option' lines) of exactly ONE actual repo (i.e., not a repo group or a +# repo pattern). + +# MOTIVATION: + +# Fedora has a huge number of repos, as well as lot of churn in permissions. +# The combination of having a large conf *and* frequent compiles were not +# working out, hence this solution. Not sure if any others have such a +# situation, so it's a standalone program, separate from "core" gitolite, +# shipped in "contrib" instead of "src". + +# SETUP: + +# It expects to run as a gitolite sub-command, which means you will need to +# copy it from contrib to src/commands, or the equivalent location inside +# LOCAL_CODE; see non-core.html in the docs for details. + +# INVOCATION: + +# It takes one argument: the name of a file that contains the new ruleset +# you want to use. (This cannot be STDIN or "-" or something). + +# example: +# +# gitolite compile-1 <file-containing-rules-for-exactly-one-repo> + +# WARNING: + +# If the main gitolite.conf changes significantly (specifically, if the +# number of effective rules in it increase quite a bit), you may have to run +# this command on ALL repos to update their individual gl-conf files. +# +# (TBD: explain this in more concrete terms) + +# ---------------------------------------------------------------------- +# THERE IS NO ERROR CHECKING ON THE WARNING ABOVE, NOR ON THE ASSUMPTIONS AND +# REQUIREMENTS BELOW. PLEASE USE CAREFULLY! +# ---------------------------------------------------------------------- + +# ASSUMPTIONS/REQUIREMENTS: + +# The file given must contain exactly one 'repo' line, with exactly one repo +# name, followed by the rules, configs, and options for that repo in the +# normal gitolite.conf syntax. + +# The file must not have any group definitions, though it may use group +# definitions already setup in the main gitolite.conf file. + +# Rules for this repo need not be already defined in the main gitolite.conf. +# If they are, they will cease to have any effect once you run this command +# - only the rules you supply in the file passed to this command will apply, +# and they will be considered to be placed at the end of gitolite.conf. + +# If the repo does not exist, it must be first created using: +# +# GL_USER=admin gitolite create <reponame> +# +# where <reponame> is the gitolite-style name (i.e., "foo", not "foo.git" or +# "~/repositories/foo" or "~/repositories/foo.git") +# +# This, of course, requires the main gitolite.conf to have the following +# lines at the top: +# +# repo [A-Za-z].* +# C = admin + +# Any change to the main gitolite.conf is followed by a full 'gitolite +# compile'; i.e., ~/.gitolite/conf/gitolite.conf-compiled.pm, the main +# "compiled" conf file, is consistent with the latest gitolite.conf. + +use 5.10.0; +use Data::Dumper; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; +use Gitolite::Conf; +use Gitolite::Conf::Store; +use Gitolite::Conf::Sugar; + +my ($cf, $repo) = args(); # conffile from @ARGV, repo from first line of conffile +my $startseq = getseq(); # get the starting sequence number by looking in the (common) compiled conf file +parse_and_store($cf, $repo); # parse the ruleset and write out just the gl-conf file + # (this is the only part that uses core gitolite functions) +update_seq($repo, $startseq); # update gl-conf with adjusted sequence numbers + +exit 0; + +# ---------------------------------------------------------------------- + +sub args { + my $cf = shift @ARGV or _die "need conffile"; + $cf = $ENV{PWD} . "/" . $cf unless $cf =~ m(^/); + + my $t = slurp($cf); + _die "bad conf file" unless $t =~ /^\s*repo\s+(\S+)\s*$/m; + my $repo = $1; + + return ($cf, $repo); +} + +sub getseq { + my @main_cc = slurp "$rc{GL_ADMIN_BASE}/conf/gitolite.conf-compiled.pm"; + my $max = 0; + for (@main_cc) { + $max = $1 if m/^ +(\d+),$/ and $max < $1; + } + + return $max; +} + +sub parse_and_store { + my ($cf, $repo) = @_; + + parse(sugar($cf)); + _chdir( $rc{GL_REPO_BASE} ); + Gitolite::Conf::Store::store_1($repo); +} + +sub update_seq { + my ($repo, $startseq) = @_; + + _chdir("$rc{GL_REPO_BASE}/$repo.git"); + my $text = slurp("gl-conf"); + + $startseq+=1000; + # just for safety, in case someone adds a few rules to the main conf later, but neglects to update repo confs + + $text =~ s/^( +)(\d+),$/"$1" . ($2+$startseq) . ","/gme; + + _print("gl-conf", $text); +} diff --git a/contrib/commands/ukm b/contrib/commands/ukm new file mode 100755 index 0000000..3683154 --- /dev/null +++ b/contrib/commands/ukm @@ -0,0 +1,732 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; +use Gitolite::Easy; + +=for usage +Usage for this command is not that simple. Please read the full +documentation in +https://github.com/sitaramc/gitolite-doc/blob/master/contrib/ukm.mkd +or online at http://gitolite.com/gitolite/ukm.html. +=cut + +usage() if @ARGV and $ARGV[0] eq '-h'; + +# Terms used in this file. +# pubkeypath: the (relative) filename of a public key starting from +# gitolite-admin/keydir. Examples: alice.pub, foo/bar/alice.pub, +# alice@home.pub, foo/alice@laptop.pub. You get more examples, if you +# replace "alice" by "bob@example.com". +# userid: computed from a pubkeypath by removing any directory +# part, the '.pub' extension and the "old-style" @NAME classifier. +# The userid identifies a user in the gitolite.conf file. +# keyid: an identifier for a key given on the command line. +# If the script is called by one of the super_key_managers, then the +# keyid is the pubkeypath without the '.pub' extension. Otherwise it +# is the userid for a guest. +# The keyid is normalized to lowercase letters. + +my $rb = $rc{GL_REPO_BASE}; +my $ab = $rc{GL_ADMIN_BASE}; + +# This will be the subdirectory under "keydir" in which the guest +# keys will be stored. To prevent denial of service, this directory +# should better start with 'zzz'. +# The actual value can be set through the GUEST_DIRECTORY resource. +# WARNING: If this value is changed you must understand the consequences. +# There will be no support if guestkeys_dir is anything else than +# 'zzz/guests'. +my $guestkeys_dir = 'zzz/guests'; + +# A guest key cannot have arbitrary names (keyid). Only keys that do *not* +# match $forbidden_guest_pattern are allowed. Super-key-managers can add +# any keyid. + +# This is the directory for additional keys of a self key manager. +my $selfkeys_dir = 'zzz/self'; +# There is no flexibility for selfkeys. One must specify a keyid that +# matches the regular expression '^@[a-z0-9]+$'. Note that all keyids +# are transformed to lowercase before checking. +my $required_self_pattern = qr([a-z0-9]+); +my $selfkey_management = 0; # disable selfkey managment + +# For guest key managers the keyid must pass two tests. +# 1) It must match the $required_guest_pattern regular expression. +# 2) It must not match the $forbidden_guest_pattern regular expression. +# Default for $forbidden_guest_pattern is qr(.), i.e., every keyid is +# forbidden, or in other words, only the gitolite-admin can manage keys. +# Default for $required_guest_pattern is such that the keyid must look +# like an email address, i.e. must have exactly one @ and at least one +# dot after the @. +# Just setting 'ukm' => 1 in .gitolite.rc only allows the super-key-managers +# (i.e., only the gitolite admin(s)) to manage keys. +my $required_guest_pattern = + qr(^[0-9a-z][-0-9a-z._+]*@[-0-9a-z._+]+[.][-0-9a-z._+]+$); +my $forbidden_guest_pattern = qr(.); + +die "The command 'ukm' is not enabled.\n" if ! $rc{'COMMANDS'}{'ukm'}; + +my $km = $rc{'UKM_CONFIG'}; +if(ref($km) eq 'HASH') { + # If not set we only allow keyids that look like emails + my $rgp = $rc{'UKM_CONFIG'}{'REQUIRED_GUEST_PATTERN'} || ''; + $required_guest_pattern = qr(^($rgp)$) if $rgp; + $forbidden_guest_pattern = $rc{'UKM_CONFIG'}{'FORBIDDEN_GUEST_PATTERN'} + || $forbidden_guest_pattern; + $selfkey_management = $rc{'UKM_CONFIG'}{'SELFKEY_MANAGEMENT'} || 0; +} + +# get the actual userid +my $gl_user = $ENV{GL_USER}; +my $super_key_manager = is_admin(); # or maybe is_super_admin() ? + +# save arguments for later +my $operation = shift || 'list'; +my $keyid = shift || ''; +$keyid = lc $keyid; # normalize to lowercase ids + +my ($zop, $zfp, $zselector, $zuser) = get_pending($gl_user); +# The following will only be true if a selfkey manager logs in to +# perform a pending operation. +my $pending_self = ($zop ne ''); + +die "You are not a key manager.\n" + unless $super_key_manager || $pending_self + || in_group('guest-key-managers') + || in_group('self-key-managers'); + +# Let's deal with the pending user first. The only allowed operations +# that are to confirm the add operation with the random code +# that must be provided via stdin or to undo a pending del operation. +if ($pending_self) { + pending_user($gl_user, $zop, $zfp, $zselector, $zuser); + exit; +} + +my @available_operations = ('list','add','del'); +die "unknown ukm subcommand: $operation\n" + unless grep {$operation eq $_} @available_operations; + +# get to the keydir +_chdir("$ab/keydir"); + +# Note that the program warns if it finds a fingerprint that maps to +# different userids. +my %userids = (); # mapping from fingerprint to userid +my %fingerprints = (); # mapping from pubkeypath to fingerprint +my %pubkeypaths = (); # mapping from userid to pubkeypaths + # note that the result is a list of pubkeypaths + +# Guest keys are managed by people in the @guest-key-managers group. +# They can only add/del keys in the $guestkeys_dir directory. In fact, +# the guest key manager $gl_user has only access to keys inside +# %guest_pubkeypaths. +my %guest_pubkeypaths = (); # mapping from userid to pubkeypath for $gl_user + +# Self keys are managed by people in the @self-key-managers group. +# They can only add/del keys in the $selfkeys_dir directory. In fact, +# the self key manager $gl_user has only access to keys inside +# %self_pubkeypaths. +my %self_pubkeypaths = (); + +# These are the keys that are managed by a super key manager. +my @all_pubkeypaths = `find . -type f -name "*.pub" 2>/dev/null | sort`; + +for my $pubkeypath (@all_pubkeypaths) { + chomp($pubkeypath); + my $fp = fingerprint($pubkeypath); + $fingerprints{$pubkeypath} = $fp; + my $userid = get_userid($pubkeypath); + my ($zop, $zfp, $zselector, $zuser) = get_pending($userid); + $userid = $zuser if $zop; + if (! defined $userids{$fp}) { + $userids{$fp} = $userid; + } else { + warn "key $fp is used for different user ids\n" + unless $userids{$fp} eq $userid; + } + push @{$pubkeypaths{$userid}}, $pubkeypath; + if ($pubkeypath =~ m|^./$guestkeys_dir/([^/]+)/[^/]+\.pub$|) { + push @{$guest_pubkeypaths{$userid}}, $pubkeypath if $gl_user eq $1; + } + if ($pubkeypath =~ m|^./$selfkeys_dir/([^/]+)/[^/]+\.pub$|) { + push @{$self_pubkeypaths{$userid}}, $pubkeypath if $gl_user eq $1; + } +} + +################################################################### +# do stuff according to the operation +################################################################### + +if ( $operation eq 'list' ) { + list_pubkeys(); + print "\n\n"; + exit; +} + +die "keyid required\n" unless $keyid; +die "Not allowed to use '..' in keyid.\n" if $keyid =~ /\.\./; + +if ( $operation eq 'add' ) { + if ($super_key_manager) { + add_pubkey($gl_user, "$keyid.pub", safe_stdin()); + } elsif (selfselector($keyid)) { + add_self($gl_user, $keyid, safe_stdin()); + } else { + # assert ingroup('guest-key-managers'); + add_guest($gl_user, $keyid, safe_stdin()); + } +} elsif ( $operation eq 'del' ) { + if ($super_key_manager) { + del_super($gl_user, "$keyid.pub"); + } elsif (selfselector($keyid)) { + del_self($gl_user, $keyid); + } else { + # assert ingroup('guest-key-managers'); + del_guest($gl_user, $keyid); + } +} + +exit; + + +################################################################### +# only function definitions are following +################################################################### + +# make a temp clone and switch to it +our $TEMPDIR; +BEGIN { $TEMPDIR = `mktemp -d -t tmp.XXXXXXXXXX`; chomp($TEMPDIR) } +END { my $err = $?; `/bin/rm -rf $TEMPDIR`; $? = $err; } + +sub cd_temp_clone { + chomp($TEMPDIR); + hushed_git( "clone", "$rb/gitolite-admin.git", "$TEMPDIR/gitolite-admin" ); + chdir("$TEMPDIR/gitolite-admin"); + my $ip = $ENV{SSH_CONNECTION}; + $ip =~ s/ .*//; + my ($zop, $zfp, $zselector, $zuser) = get_pending($ENV{GL_USER}); + my $email = $zuser; + $email .= '@' . $ip unless $email =~ m(@); + my $name = $zop ? "\@$zselector" : $zuser; + # Record the keymanager in the gitolite-admin repo as author of the change. + hushed_git( "config", "user.email", "$email" ); + hushed_git( "config", "user.name", "'$name from $ip'" ); +} + +# compute the fingerprint from the full path of a pubkey file +sub fingerprint { + my ($fp, $output) = ssh_fingerprint_file(shift); + # Do not print the output of $output to an untrusted destination. + die "does not seem to be a valid pubkey\n" unless $fp; + return $fp; +} + + +# Read one line from STDIN and return it. +# If no data is available on STDIN after one second, the empty string +# is returned. +# If there is more than one line or there was an error in reading, the +# function dies. +sub safe_stdin { + use IO::Select; + my $s=IO::Select->new(); $s->add(\*STDIN); + return '' unless $s->can_read(1); + my $data; + my $ret = read STDIN, $data, 4096; + # current pubkeys are approx 400 bytes so we go a little overboard + die "could not read pubkey data" . ( defined($ret) ? "" : ": $!" ) . "\n" + unless $ret; + die "pubkey data seems to have more than one line\n" if $data =~ /\n./; + return $data; +} + +# call git, be quiet +sub hushed_git { + system("git " . join(" ", @_) . ">/dev/null 2>/dev/null"); +} + +# Extract the userid from the full path of the pubkey file (relative +# to keydir/ and including the '.pub' extension. +sub get_userid { + my ($u) = @_; # filename of pubkey relative to keydir/. + $u =~ s(.*/)(); # foo/bar/baz.pub -> baz.pub + $u =~ s/(\@[^.]+)?\.pub$//; # baz.pub, baz@home.pub -> baz + return $u; +} + +# Extract the @selector part from the full path of the pubkey file +# (relative to keydir/ and including the '.pub' extension). +# If there is no @selector part, the empty string is returned. +# We also correctly extract the selector part from pending keys. +sub get_selector { + my ($u) = @_; # filename of pubkey relative to keydir/. + $u =~ s(.*/)(); # foo/bar/baz.pub -> baz.pub + $u =~ s(\.pub$)(); # baz@home.pub -> baz@home + return $1 if $u =~ m/.\@($required_self_pattern)$/; # baz@home -> home + my ($zop, $zfp, $zselector, $zuser) = get_pending($u); + # If $u was not a pending key, then $zselector is the empty string. + return $zselector; +} + +# Extract fingerprint, operation, selector, and true userid from a +# pending userid. +sub get_pending { + my ($gl_user) = @_; + return ($1, $2, $3, $4) + if ($gl_user=~/^zzz-(...)-([0-9a-f]{32})-($required_self_pattern)-(.*)/); + return ('', '', '', $gl_user) +} + +# multiple / and are simplified to one / and the path is made relative +sub sanitize_pubkeypath { + my ($pubkeypath) = @_; + $pubkeypath =~ s|//|/|g; # normalize path + $pubkeypath =~ s,\./,,g; # remove './' from path + return './'.$pubkeypath; # Don't allow absolute paths. +} + +# This function is only relavant for guest key managers. +# It returns true if the pattern is OK and false otherwise. +sub required_guest_keyid { + my ($_) = @_; + /$required_guest_pattern/ and ! /$forbidden_guest_pattern/; +} + +# The function takes a $keyid as input and returns the keyid with the +# initial @ stripped if everything is fine. It aborts with an error if +# selfkey management is not enabled or the function is called for a +# non-self-key-manager. +# If the required selfkey pattern is not matched, it returns an empty string. +# Thus the function can be used to check whether a given keyid is a +# proper selfkeyid. +sub selfselector { + my ($keyid) = @_; + return '' unless $keyid =~ m(^\@($required_self_pattern)$); + $keyid = $1; + die "selfkey management is not enabled\n" unless $selfkey_management; + die "You are not a selfkey manager.\n" if ! in_group('self-key-managers'); + return $keyid; +} + +# Return the number of characters reserved for the userid field. +sub userid_width { + my ($paths) = @_; + my (%pkpaths) = %{$paths}; + my (@userid_lengths) = sort {$a <=> $b} (map {length($_)} keys %pkpaths); + @userid_lengths ? $userid_lengths[-1] : 0; +} + +# List the keys given by a reference to a hash. +# The regular expression $re is used to remove the initial part of the +# keyid and replace it by what is matched inside the parentheses. +# $format and $width are used for pretty printing +sub list_keys { + my ($paths, $tokeyid, $format, $width) = @_; + my (%pkpaths) = %{$paths}; + for my $userid (sort keys %pkpaths) { + for my $pubkeypath (sort @{$pkpaths{$userid}}) { + my $fp = $fingerprints{$pubkeypath}; + my $userid = $userids{$fp}; + my $keyid = &{$tokeyid}($pubkeypath); + printf $format,$fp,$userid,$width+1-length($userid),"",$keyid + if ($super_key_manager + || required_guest_keyid($keyid) + || $keyid=~m(^\@)); + } + } +} + +# Turn a pubkeypath into a keyid for super-key-managers, guest-keys, +# and self-keys. +sub superkeyid { + my ($keyid) = @_; + $keyid =~ s(\.pub$)(); + $keyid =~ s(^\./)(); + return $keyid; +} + +sub guestkeyid { + my ($keyid) = @_; + $keyid =~ s(\.pub$)(); + $keyid =~ s(^.*/)(); + return $keyid; +} + +sub selfkeyid { + my ($keyid) = @_; + $keyid =~ s(\.pub$)(); + $keyid =~ s(^.*/)(); + my ($zop, $zfp, $zselector, $zuser) = get_pending($keyid); + return "\@$zselector (pending $zop)" if $zop; + $keyid =~ s(.*@)(@); + return $keyid; +} + +################################################################### + +# List public keys managed by the respective user. +# The fingerprints, userids and keyids are printed. +# keyids are shown in a form that can be used for add and del +# subcommands. +sub list_pubkeys { + print "Hello $gl_user, you manage the following keys:\n"; + my $format = "%-47s %s%*s%s\n"; + my $width = 0; + if ($super_key_manager) { + $width = userid_width(\%pubkeypaths); + $width = 6 if $width < 6; # length("userid")==6 + printf $format, "fingerprint", "userid", ($width-5), "", "keyid"; + list_keys(\%pubkeypaths, , \&superkeyid, $format, $width); + } else { + my $widths = $selfkey_management?userid_width(\%self_pubkeypaths):0; + my $widthg = userid_width(\%guest_pubkeypaths); + $width = $widths > $widthg ? $widths : $widthg; # maximum width + return unless $width; # there are no keys + $width = 6 if $width < 6; # length("userid")==6 + printf $format, "fingerprint", "userid", ($width-5), "", "keyid"; + list_keys(\%self_pubkeypaths, \&selfkeyid, $format, $width) + if $selfkey_management; + list_keys(\%guest_pubkeypaths, \&guestkeyid, $format, $width); + } +} + + +################################################################### + +# Add a public key for the user $gl_user. +# $pubkeypath is the place where the new key will be stored. +# If the file or its fingerprint already exists, the operation is +# rejected. +sub add_pubkey { + my ( $gl_user, $pubkeypath, $keymaterial ) = @_; + if(! $keymaterial) { + print STDERR "Please supply the new key on STDIN.\n"; + print STDERR "Try something like this:\n"; + print STDERR "cat FOO.pub | ssh GIT\@GITOLITESERVER ukm add KEYID\n"; + die "missing public key data\n"; + } + # clean pubkeypath a bit + $pubkeypath = sanitize_pubkeypath($pubkeypath); + # Check that there is not yet something there already. + die "cannot override existing key\n" if $fingerprints{$pubkeypath}; + + my $userid = get_userid($pubkeypath); + # Super key managers shouldn't be able to add a that leads to + # either an empty userid or to a userid that starts with @. + # + # To avoid confusion, all keyids for super key managers must be in + # a full path format. Having a public key of the form + # gitolite-admin/keydir/@foo.pub might be confusing and might lead + # to other problems elsewhere. + die "cannot add key that starts with \@\n" if (!$userid) || $userid=~/^@/; + + cd_temp_clone(); + _chdir("keydir"); + $pubkeypath =~ m((.*)/); # get the directory part + _mkdir($1); + _print($pubkeypath, $keymaterial); + my $fp = fingerprint($pubkeypath); + + # Maybe we are adding a selfkey. + my ($zop, $zfp, $zselector, $zuser) = get_pending($userid); + my $user = $zop ? "$zuser\@$zselector" : $userid; + $userid = $zuser; + # Check that there isn't a key with the same fingerprint under a + # different userid. + if (defined $userids{$fp}) { + if ($userid ne $userids{$fp}) { + print STDERR "Found $fp $userids{$fp}\n" if $super_key_manager; + print STDERR "Same key is already available under another userid.\n"; + die "cannot add key\n"; + } elsif ($zop) { + # Because of the way a key is confirmed with ukm, it is + # impossible to confirm the initial key of the user as a + # new selfkey. (It will lead to the function list_pubkeys + # instead of pending_user_add, because the gl_user will + # not be that of a pending user.) To avoid confusion, we, + # therefore, forbid to add the user's initial key + # altogether. + # In fact, we here also forbid to add any key for that + # user that is already in the system. + die "You cannot add a key that already belongs to you.\n"; + } + } else {# this fingerprint does not yet exist + my @paths = @{$pubkeypaths{$userid}} if defined $pubkeypaths{$userid}; + if (@paths) {# there are already keys for $userid + if (grep {$pubkeypath eq $_} @paths) { + print STDERR "The keyid is already present. Nothing changed.\n"; + } elsif ($super_key_manager) { + # It's OK to add new selfkeys, but here we are in the case + # of adding multiple keys for guests. That is forbidden. + print STDERR "Adding new public key for $userid.\n"; + } elsif ($pubkeypath =~ m(^\./$guestkeys_dir/)) { + # Arriving here means we are about to add a *new* + # guest key, because the fingerprint is not yet + # existing. This would be for an already existing + # userid (added by another guest key manager). Since + # that effectively means to (silently) add an + # additional key for an existing user, it must be + # forbidden. + die "cannot add another public key for an existing user\n"; + } + } + } + exit if (`git status -s` eq ''); # OK to add identical keys twice + hushed_git( "add", "." ) and die "git add failed\n"; + hushed_git( "commit", "-m", "'ukm add $gl_user $userid\n\n$fp'" ) + and die "git commit failed\n"; + system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; +} + +# Guest key managers should not be allowed to add directories or +# multiple keys via the @domain mechanism, since this might allow +# another guest key manager to give an attacker access to another +# user's repositories. +# +# Example: Alice adds bob.pub for bob@example.org. David adds eve.pub +# (where only Eve but not Bob has the private key) under the keyid +# bob@example.org@foo. This basically gives Eve the same rights as +# Bob. +sub add_guest { + my ( $gl_user, $keyid, $keymaterial ) = @_; + die "keyid not allowed: '$keyid'\n" + if $keyid =~ m(@.*@) or $keyid =~ m(/) or !required_guest_keyid($keyid); + add_pubkey($gl_user, "$guestkeys_dir/$gl_user/$keyid.pub", $keymaterial); +} + +# Add a new selfkey for user $gl_user. +sub add_self { + my ( $gl_user, $keyid, $keymaterial ) = @_; + my $selector = ""; + $selector = selfselector($keyid); # might return empty string + die "keyid not allowed: $keyid\n" unless $selector; + + # Check that the new selector is not already in use even not in a + # pending state. + die "keyid already in use: $keyid\n" + if grep {selfkeyid($_)=~/^\@$selector( .*)?$/} @{$self_pubkeypaths{$gl_user}}; + # generate new pubkey create fingerprint + system("ssh-keygen -N '' -q -f \"$TEMPDIR/session\" -C $gl_user"); + my $sessionfp = fingerprint("$TEMPDIR/session.pub"); + $sessionfp =~ s/://g; + my $user = "zzz-add-$sessionfp-$selector-$gl_user"; + add_pubkey($gl_user, "$selfkeys_dir/$gl_user/$user.pub", $keymaterial); + print `cat "$TEMPDIR/session.pub"`; +} + +################################################################### + + +# Delete a key of user $gl_user. +sub del_pubkey { + my ($gl_user, $pubkeypath) = @_; + $pubkeypath = sanitize_pubkeypath($pubkeypath); + my $fp = $fingerprints{$pubkeypath}; + die "key not found\n" unless $fp; + cd_temp_clone(); + chdir("keydir"); + hushed_git( "rm", "$pubkeypath" ) and die "git rm failed\n"; + my $userid = get_userid($pubkeypath); + hushed_git( "commit", "-m", "'ukm del $gl_user $userid\n\n$fp'" ) + and die "git commit failed\n"; + system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; +} + +# $gl_user is a super key manager. This function aborts if the +# superkey manager tries to remove his last key. +sub del_super { + my ($gl_user, $pubkeypath) = @_; + $pubkeypath = sanitize_pubkeypath($pubkeypath); + die "You are not managing the key $keyid.\n" + unless grep {$_ eq $pubkeypath} @all_pubkeypaths; + my $userid = get_userid($pubkeypath); + if ($gl_user eq $userid) { + my @paths = @{$pubkeypaths{$userid}}; + die "You cannot delete your last key.\n" + if scalar(grep {$userid eq get_userid($_)} @paths)<2; + } + del_pubkey($gl_user, $pubkeypath); +} + +sub del_guest { + my ($gl_user, $keyid) = @_; + my $pubkeypath = sanitize_pubkeypath("$guestkeys_dir/$gl_user/$keyid.pub"); + my $userid = get_userid($pubkeypath); + # Check whether $gl_user actually manages $keyid. + my @paths = (); + @paths = @{$guest_pubkeypaths{$userid}} + if defined $guest_pubkeypaths{$userid}; + die "You are not managing the key $keyid.\n" + unless grep {$_ eq $pubkeypath} @paths; + del_pubkey($gl_user, $pubkeypath); +} + +# Delete a selfkey of $gl_user. The first delete is a preparation of +# the deletion and only a second call will actually delete the key. If +# the second call is done with the key that is scheduled for deletion, +# it is basically undoing the previous del call. This last case is +# handled in function pending_user_del. +sub del_self { + my ($gl_user, $keyid) = @_; + my $selector = selfselector($keyid); # might return empty string + die "keyid not allowed: '$keyid'\n" unless $selector; + + # Does $gl_user actually manage that keyid? + # All (non-pending) selfkeys have an @selector part in their pubkeypath. + my @paths = @{$self_pubkeypaths{$gl_user}}; + die "You are not managing the key $keyid.\n" + unless grep {$selector eq get_selector($_)} @paths; + + cd_temp_clone(); + _chdir("keydir"); + my $fp = ''; + # Is it the first or the second del call? It's the second call, if + # there is a scheduled-for-deletion or scheduled-for-addition + # selfkey which has the given keyid as a selector part. + @paths = grep { + my ($zop, $zfp, $zselector, $zuser) = get_pending(get_userid($_)); + $zselector eq $selector + } @paths; + if (@paths) {# start actual deletion of the key (second call) + my $pubkeypath = $paths[0]; + $fp = fingerprint($pubkeypath); + my ($zop, $zf, $zs, $zu) = get_pending(get_userid($pubkeypath)); + $zop = $zop eq 'add' ? 'undo-add' : 'confirm-del'; + hushed_git("rm", "$pubkeypath") and die "git rm failed\n"; + hushed_git("commit", "-m", "'ukm $zop $gl_user\@$selector\n\n$fp'") + and die "git commit failed\n"; + system("gitolite push >/dev/null 2>/dev/null") + and die "git push failed\n"; + print STDERR "pending keyid deleted: \@$selector\n"; + return; + } + my $oldpubkeypath = "$selfkeys_dir/$gl_user/$gl_user\@$selector.pub"; + # generate new pubkey and create fingerprint to get a random number + system("ssh-keygen -N '' -q -f \"$TEMPDIR/session\" -C $gl_user"); + my $sessionfp = fingerprint("$TEMPDIR/session.pub"); + $sessionfp =~ s/://g; + my $user = "zzz-del-$sessionfp-$selector-$gl_user"; + my $newpubkeypath = "$selfkeys_dir/$gl_user/$user.pub"; + + # A key for gitolite access that is in authorized_keys and not + # existing in the expected place under keydir/ should actually not + # happen, but one never knows. + die "key not available\n" unless -r $oldpubkeypath; + + # For some strange reason the target key already exists. + die "cannot override existing key\n" if -e $newpubkeypath; + + $fp = fingerprint($oldpubkeypath); + print STDERR "prepare deletion of key \@$selector\n"; + hushed_git("mv", "$oldpubkeypath", "$newpubkeypath") + and die "git mv failed\n"; + hushed_git("commit", "-m", "'ukm prepare-del $gl_user\@$selector\n\n$fp'") + and die "git commit failed\n"; + system("gitolite push >/dev/null 2>/dev/null") + and die "git push failed\n"; +} + +################################################################### +# Adding a selfkey should be done as follows. +# +# cat newkey.pub | ssh git@host ukm add @selector > session +# cat session | ssh -i newkey git@host ukm +# +# The provided random data will come from a newly generated ssh key +# whose fingerprint will be stored in $gl_user. So we compute the +# fingerprint of the data that is given to us. If it doesn't match the +# fingerprint, then something went wrong and the confirm operation is +# forbidden, in fact, the pending key will be removed from the system. +sub pending_user_add { + my ($gl_user, $zfp, $zselector, $zuser) = @_; + my $oldpubkeypath = "$selfkeys_dir/$zuser/$gl_user.pub"; + my $newpubkeypath = "$selfkeys_dir/$zuser/$zuser\@$zselector.pub"; + + # A key for gitolite access that is in authorized_keys and not + # existing in the expected place under keydir/ should actually not + # happen, but one never knows. + die "key not available\n" unless -r $oldpubkeypath; + + my $keymaterial = safe_stdin(); + # If there is no keymaterial (which corresponds to a session key + # for the confirm-add operation), logging in to this key, removes + # it from the system. + my $session_key_not_provided = ''; + if (!$keymaterial) { + $session_key_not_provided = "missing session key"; + } else { + _print("$TEMPDIR/session.pub", $keymaterial); + my $sessionfp = fingerprint("$TEMPDIR/session.pub"); + $sessionfp =~ s/://g; + $session_key_not_provided = "session key not accepted" + unless ($zfp eq $sessionfp) + } + my $fp = fingerprint($oldpubkeypath); + if ($session_key_not_provided) { + print STDERR "$session_key_not_provided\n"; + print STDERR "pending keyid deleted: \@$zselector\n"; + hushed_git("rm", "$oldpubkeypath") and die "git rm failed\n"; + hushed_git("commit", "-m", "'ukm del $zuser\@$zselector\n\n$fp'") + and die "git commit failed\n"; + system("gitolite push >/dev/null 2>/dev/null") + and die "git push failed\n"; + return; + } + + # For some strange reason the target key already exists. + die "cannot override existing key\n" if -e $newpubkeypath; + + print STDERR "pending keyid added: \@$zselector\n"; + hushed_git("mv", "$oldpubkeypath", "$newpubkeypath") + and die "git mv failed\n"; + hushed_git("commit", "-m", "'ukm confirm-add $zuser\@$zselector\n\n$fp'") + and die "git commit failed\n"; + system("gitolite push >/dev/null 2>/dev/null") + and die "git push failed\n"; +} + +# To delete a key, one must first bring the key into a pending state +# and then truely delete it with another key. In case, the login +# happens with the pending key (implemented below), it means that the +# delete operation has to be undone. +sub pending_user_del { + my ($gl_user, $zfp, $zselector, $zuser) = @_; + my $oldpubkeypath = "$selfkeys_dir/$zuser/$gl_user.pub"; + my $newpubkeypath = "$selfkeys_dir/$zuser/$zuser\@$zselector.pub"; + print STDERR "undo pending deletion of keyid \@$zselector\n"; + # A key for gitolite access that is in authorized_keys and not + # existing in the expected place under keydir/ should actually not + # happen, but one never knows. + die "key not available\n" unless -r $oldpubkeypath; + # For some strange reason the target key already exists. + die "cannot override existing key\n" if -e $newpubkeypath; + my $fp = fingerprint($oldpubkeypath); + hushed_git("mv", "$oldpubkeypath", "$newpubkeypath") + and die "git mv failed\n"; + hushed_git("commit", "-m", "'ukm undo-del $zuser\@$zselector\n\n$fp'") + and die "git commit failed\n"; +} + +# A user whose key is in pending state cannot do much. In fact, +# logging in as such a user simply takes back the "bringing into +# pending state", i.e. a key scheduled for adding is remove and a key +# scheduled for deletion is brought back into its properly added state. +sub pending_user { + my ($gl_user, $zop, $zfp, $zselector, $zuser) = @_; + cd_temp_clone(); + _chdir("keydir"); + if ($zop eq 'add') { + pending_user_add($gl_user, $zfp, $zselector, $zuser); + } elsif ($zop eq 'del') { + pending_user_del($gl_user, $zfp, $zselector, $zuser); + } else { + die "unknown operation\n"; + } + system("gitolite push >/dev/null 2>/dev/null") + and die "git push failed\n"; +} |