diff options
-rwxr-xr-x | contrib/commands/ukm | 732 | ||||
-rw-r--r-- | contrib/t/ukm.t | 447 |
2 files changed, 1179 insertions, 0 deletions
diff --git a/contrib/commands/ukm b/contrib/commands/ukm new file mode 100755 index 0000000..8a2d361 --- /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 = `ssh-keygen -l -f $_[0]`; + die "does not seem to be a valid pubkey\n" + unless $fp =~ /(([0-9a-f]+:)+[0-9a-f]+) /i; + return $1; +} + + +# 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"; +} diff --git a/contrib/t/ukm.t b/contrib/t/ukm.t new file mode 100644 index 0000000..da4fc0b --- /dev/null +++ b/contrib/t/ukm.t @@ -0,0 +1,447 @@ +#!/usr/bin/perl + +# Call like this: +# TSH_VERBOSE=1 TSH_ERREXIT=1 HARNESS_ACTIVE=1 GITOLITE_TEST=y prove t/ukm.t + +use strict; +use warnings; + +# this is hardcoded; change it if needed +use lib "src/lib"; +use Gitolite::Common; +use Gitolite::Test; + +# basic tests using ssh +# ---------------------------------------------------------------------- + +my $bd = `gitolite query-rc -n GL_BINDIR`; +my $h = $ENV{HOME}; +my $ab = `gitolite query-rc -n GL_ADMIN_BASE`; +my $pd = "$bd/../t/keys"; # source for pubkeys +umask 0077; + +_mkdir( "$h/.ssh", 0700 ) if not -d "$h/.ssh"; + +try "plan 204"; + + +# Reset everything. +# Only admin and u1, u2, and u3 keys are available initially +# Keys u4, u5, and u6 are used as guests later. +# For easy access, we put the keys into ~/.ssh/, though. +try " + rm -f $h/.ssh/authorized_keys; ok or die 1 + cp $pd/u[1-6]* $h/.ssh; ok or die 2 + cp $pd/admin* $h/.ssh; ok or die 3 + cp $pd/config $h/.ssh; ok or die 4 + cat $h/.ssh/config + perl s/%USER/$ENV{USER}/ + put $h/.ssh/config + mkdir $ab/keydir; ok or die 5 + cp $pd/u[1-3].pub $ab/keydir; ok or die 6 + cp $pd/admin.pub $ab/keydir; ok or die 7 +"; + +# Put the keys into ~/.ssh/authorized_keys +system("gitolite ../triggers/post-compile/ssh-authkeys"); + +# enable user key management in a simple form. +# Guest key managers can add keyids looking like email addresses, but +# cannot add emails containing example.com or hemmecke.org. +system("sed -i \"s/.*ENABLE =>.*/'UKM_CONFIG'=>{'FORBIDDEN_GUEST_PATTERN'=>'example.com|hemmecke.org'}, ENABLE => ['ukm',/\" $h/.gitolite.rc"); + +# super-key-managers can add/del any key +# super-key-managers should in fact agree with people having write +# access to gitolite-admin repo. +# guest-key-managers can add/del guest keys +confreset; confadd ' + @guest-key-managers = u2 u3 + @creators = u2 u3 + repo pub/CREATOR/..* + C = @creators + RW+ = CREATOR + RW = WRITERS + R = READERS +'; + +# Populate the gitolite-admin/keydir in the same way as it was used for +# the initialization of .ssh/authorized_keys above. +try " + mkdir keydir; ok or die 8 + cp $pd/u[1-3].pub keydir; ok or die 9; + cp $pd/admin.pub keydir; ok or die 10; + git add conf keydir; ok + git commit -m ukm; ok; /master.* ukm/ +"; + +# Activate new config data. +try "PUSH admin; ok; gsh; /master -> master/; !/FATAL/" or die text(); + +# Check whether the above setup yields the expected behavior for ukm. +# The admin is super-key-manager, thus can manage every key. +try " + ssh admin ukm; ok; /Hello admin, you manage the following keys:/ + / admin +admin/ + / u1 +u1/ + / u2 +u2/ + / u3 +u3/ +"; + +# u1 isn't a key manager, so shouldn't be above to manage keys. +try "ssh u1 ukm; !ok; /FATAL: You are not a key manager./"; + +# u2 and u3 are guest key managers, but don't yet manage any key. +try "ssh u2 ukm; ok"; cmp "Hello u2, you manage the following keys:\n\n\n"; +try "ssh u3 ukm; ok"; cmp "Hello u3, you manage the following keys:\n\n\n"; + + +################################################################### +# Unknows subkommands abort ukm. +try "ssh u2 ukm fake; !ok; /FATAL: unknown ukm subcommand: fake/"; + + +################################################################### +# Addition of keys. + +# If no data is provided on stdin, we don't block, but rather timeout +# after one second and abort the program. +try "ssh u2 ukm add u4\@example.org; !ok; /FATAL: missing public key data/"; + +# If no keyid is given, we cannot add a key. +try "ssh u2 ukm add; !ok; /FATAL: keyid required/"; + +try " + DEF ADD = cat $pd/%1.pub|ssh %2 ukm add %3 + DEF ADDOK = ADD %1 %2 %3; ok + DEF ADDNOK = ADD %1 %2 %3; !ok + DEF FP = ADDNOK u4 u2 %1 + DEF FORBIDDEN_PATTERN = FP %1; /FATAL: keyid not allowed:/ +"; + +# Neither a guest key manager nor a super key manager can add keys that have +# double dot in their keyid. This is hardcoded to forbid paths with .. in it. +try " + ADDNOK u4 u2 u4\@hemmecke..org; /Not allowed to use '..' in keyid./ + ADDNOK u4 admin u4\@hemmecke..org; /Not allowed to use '..' in keyid./ + ADDNOK u4 admin ./../.myshrc; /Not allowed to use '..' in keyid./ +"; + +# guest-key-managers can only add keys that look like emails. +try " + FORBIDDEN_PATTERN u4 + FORBIDDEN_PATTERN u4\@example + FORBIDDEN_PATTERN u4\@foo\@example.org + + # No support for 'old style' multiple keys. + FORBIDDEN_PATTERN u4\@example.org\@foo + + # No path delimiter in keyid + FORBIDDEN_PATTERN foo/u4\@example.org + + # Certain specific domains listed in FORBIDDEN_GUEST_PATTERN are forbidden. + # Note that also u4\@example-com would be rejected, because MYDOMAIN + # contains a regular expression --> I don't care. + FORBIDDEN_PATTERN u4\@example.com + FORBIDDEN_PATTERN u4\@hemmecke.org +"; + +# Accept one guest key. +try "ADDOK u4 u2 u4\@example.org"; +try "ssh u2 ukm; ok; /Hello u2, you manage the following keys:/ + / u4\@example.org *u4\@example.org/"; + +# Various ways how a key must be rejected. +try " + # Cannot add the same key again. + ADDNOK u4 u2 u4\@example.org; /FATAL: cannot override existing key/ + + # u2 can also not add u4.pub under another keyid + ADDNOK u4 u2 u4\@example.net; /FATAL: cannot add key/ + /Same key is already available under another userid./ + + # u2 can also not add another key under the same keyid. + ADDNOK u5 u2 u4\@example.org; /FATAL: cannot override existing key/ + + # Also u3 cannot not add another key under the same keyid. + ADDNOK u5 u3 u4\@example.org + /FATAL: cannot add another public key for an existing user/ + + # And u3 cannot not add u4.pub under another keyid. + ADDNOK u4 u3 u4\@example.net; /FATAL: cannot add key/ + /Same key is already available under another userid./ + + # Not even the admin can add the same key u4 under a different userid. + ADDNOK u4 admin u4\@example.net; /FATAL: cannot add key/ + /Same key is already available under another userid./ + /Found .* u4\@example.org/ + + # Super key managers cannot add keys that start with @. + # We don't care about @ in the dirname, though. + ADDNOK u4 admin foo/\@ex.net; /FATAL: cannot add key that starts with \@/ + ADDNOK u4 admin foo/\@ex; /FATAL: cannot add key that starts with \@/ + ADDNOK u4 admin \@ex.net; /FATAL: cannot add key that starts with \@/ + ADDNOK u4 admin \@ex; /FATAL: cannot add key that starts with \@/ +"; + +# But u3 can add u4.pub under the same keyid. +try "ADDOK u4 u3 u4\@example.org"; + +try "ssh u3 ukm; ok; /Hello u3, you manage the following keys:/ + / u4\@example.org *u4\@example.org/"; + +# The admin can add multiple keys for the same userid. +try " + ADDOK u5 admin u4\@example.org + ADDOK u5 admin u4\@example.org\@home + ADDOK u5 admin laptop/u4\@example.org + ADDOK u5 admin laptop/u4\@example.org\@home +"; + +# And admin can also do this for other guest key managers. Note, +# however, that the gitolite-admin must be told where the +# GUEST_DIRECTORY is. But he/she could find out by cloning the +# gitolite-admin repository and adding the same key directly. +try " + ADDOK u5 admin zzz/guests/u2/u4\@example.org\@foo + ADDOK u6 admin zzz/guests/u3/u6\@example.org +"; + +try "ssh admin ukm; ok"; cmp "Hello admin, you manage the following keys: +fingerprint userid keyid +a4:d1:11:1d:25:5c:55:9b:5f:91:37:0e:44:a5:a5:f2 admin admin +00:2c:1f:dd:a3:76:5a:1e:c4:3c:01:15:65:19:a5:2e u1 u1 +69:6f:b5:8a:f5:7b:d8:40:ce:94:09:a2:b8:95:79:5b u2 u2 +26:4b:20:24:98:a4:e4:a5:b9:97:76:9a:15:92:27:2d u3 u3 +78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org laptop/u4\@example.org +78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org laptop/u4\@example.org\@home +78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org u4\@example.org +78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org u4\@example.org\@home +8c:a6:c0:a5:71:85:0b:89:d3:08:97:22:ae:95:e1:bb u4\@example.org zzz/guests/u2/u4\@example.org +78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org zzz/guests/u2/u4\@example.org\@foo +8c:a6:c0:a5:71:85:0b:89:d3:08:97:22:ae:95:e1:bb u4\@example.org zzz/guests/u3/u4\@example.org +fc:0f:eb:52:7a:d2:35:da:89:96:f5:15:0e:85:46:e7 u6\@example.org zzz/guests/u3/u6\@example.org +\n\n"; + +# Now, u2 has two keys in his directory, but u2 can manage only one of +# them, since the one added by the admin has two @ in it. Thus the key +# added by admin is invisible to u2. +try "ssh u2 ukm; ok"; cmp "Hello u2, you manage the following keys: +fingerprint userid keyid +8c:a6:c0:a5:71:85:0b:89:d3:08:97:22:ae:95:e1:bb u4\@example.org u4\@example.org +\n\n"; + +# Since admin added key u6@example.org to the directory of u2, u2 is +# also able to see it and, in fact, to manage it. +try "ssh u3 ukm; ok"; cmp "Hello u3, you manage the following keys: +fingerprint userid keyid +8c:a6:c0:a5:71:85:0b:89:d3:08:97:22:ae:95:e1:bb u4\@example.org u4\@example.org +fc:0f:eb:52:7a:d2:35:da:89:96:f5:15:0e:85:46:e7 u6\@example.org u6\@example.org +\n\n"; + +################################################################### +# Deletion of keys. +try " + DEF DEL = ssh %1 ukm del %2 + DEF DELOK = DEL %1 %2; ok + DEF DELNOK = DEL %1 %2; !ok + DEF DELNOMGR = DELNOK %1 %2; /FATAL: You are not managing the key / +"; + +# Deletion requires a keyid. +try "ssh u3 ukm del; !ok; /FATAL: keyid required/"; + +# u3 can, of course, not remove any unmanaged key. +try "DELNOMGR u3 u2"; + +# But u3 can delete u4@example.org and u6@example.org. This will, of course, +# not remove the key u4@example.org that u2 manages. +try " + DELOK u3 u4\@example.org + DELOK u3 u6\@example.org +"; + +# After having deleted u4@example.org, u3 cannot remove it again, +# even though, u2 still manages that key. +try "DELNOMGR u3 u4\@example.org"; + +# Of course a super-key-manager can remove any (existing) key. +try " + DELOK admin zzz/guests/u2/u4\@example.org + DELNOK admin zzz/guests/u2/u4\@example.org + /FATAL: You are not managing the key zzz/guests/u2/u4\@example.org./ + DELNOK admin zzz/guests/u2/u4\@example.org\@x + /FATAL: You are not managing the key zzz/guests/u2/u4\@example.org./ + DELOK admin zzz/guests/u2/u4\@example.org\@foo +"; + +# As the admin could do that via pushing to the gitolite-admin manually, +# it's also allowed to delete even non-guest keys. +try "DELOK admin u3"; + +# Let's clean the environment again. +try " + DELOK admin laptop/u4\@example.org\@home + DELOK admin laptop/u4\@example.org + DELOK admin u4\@example.org\@home + DELOK admin u4\@example.org + ADDOK u3 admin u3 + "; + +# Currently the admin has just one key. It cannot be removed. +# But after adding another key, deletion should work fine. +try " + DELNOK admin admin; /FATAL: You cannot delete your last key./ + ADDOK u6 admin second/admin; /Adding new public key for admin./ + DELOK admin admin + DELNOK u6 admin; /FATAL: You are not managing the key admin./ + DELNOK u6 second/admin; /FATAL: You cannot delete your last key./ + ADDOK admin u6 admin; /Adding new public key for admin./ + DELOK u6 second/admin +"; + +################################################################### +# Selfkey management. + +# If self key management is not switched on in the .gitolite.rc file, +# it's not allowed at all. +try "ssh u2 ukm add \@second; !ok; /FATAL: selfkey management is not enabled/"; + +# Let's enable it. +system("sed -i \"/'UKM_CONFIG'=>/s/=>{/=>{'SELFKEY_MANAGEMENT'=>1,/\" $h/.gitolite.rc"); + +# And add self-key-managers to gitolite.conf +# chdir("../gitolite-admin") or die "in `pwd`, could not cd ../g-a"; +try "glt pull admin origin master; ok"; +put "|cut -c5- > conf/gitolite.conf", ' + repo gitolite-admin + RW+ = admin + repo testing + RW+ = @all + @guest-key-managers = u2 u3 + @self-key-managers = u1 u2 + @creators = u2 u3 + repo pub/CREATOR/..* + C = @creators + RW+ = CREATOR + RW = WRITERS + R = READERS +'; +try " + git add conf keydir; ok + git commit -m selfkey; ok; /master.* selfkey/ +"; +try "PUSH admin; ok; gsh; /master -> master/; !/FATAL/" or die text(); + +# Now we can start with the tests. + +# Only self key managers are allowed to use selfkey management. +# See variable @self-key-managers. +try "ssh u3 ukm add \@second; !ok; /FATAL: You are not a selfkey manager./"; + +# Cannot add keyid that are not alphanumeric. +try "ssh u1 ukm add \@second-key; !ok; /FATAL: keyid not allowed:/"; + +# Add a second key for u1, but leave it pending by not feeding in the +# session key. The new user can login, but he/she lives under a quite +# random gl_user name and thus is pretty much excluded from everything +# except permissions given to @all. If this new id calls ukm without +# providing the session key, this (pending) key is automatically +# removed from the system. +# If a certain keyid is in the system, then it cannot be added again. +try " + ADDOK u4 u1 \@second + ssh admin ukm; ok; /u1 zzz/self/u1/zzz-add-[a-z0-9]{32}-second-u1/ + ssh u1 ukm; ok; /u1 \@second .pending add./ + ADDNOK u4 u1 \@second; /FATAL: keyid already in use: \@second/ + ssh u4 ukm; ok; /pending keyid deleted: \@second/ + ssh admin ukm; ok; !/zzz/; !/second/ +"; + +# Not providing a proper ssh public key will abort. Providing a good +# ssh public key, which is not a session key makes the key invalid. +# The key will, therefore, be deleted by this operation. +try " + ADDOK u4 u1 \@second + echo fake|ssh u4 ukm; !ok; /FATAL: does not seem to be a valid pubkey/ + cat $pd/u5.pub | ssh u4 ukm; ok; + /session key not accepted/ + /pending keyid deleted: \@second/ +"; + +# True addition of a new selfkey is done via piping it to a second ssh +# call that uses the new key to call ukm. Note that the first ssh must +# have completed its job before the second ssh is able to successfully +# log in. This can be done via sleep or via redirecting to a file and +# then reading from it. +try " + # ADDOK u4 u1 \@second | (sleep 2; ssh u4 ukm); ok + ADD u4 u1 \@second > session; ok + cat session | ssh u4 ukm; ok; /pending keyid added: \@second/ +"; + +# u1 cannot add his/her initial key, since that key can never be +# confirmed via ukm, so it is forbidden altogether. In fact, u1 is not +# allowed to add any key twice. +try " + ADDNOK u1 u1 \@first + /FATAL: You cannot add a key that already belongs to you./ + ADDNOK u4 u1 \@first + /FATAL: You cannot add a key that already belongs to you./ +"; + +# u1 also can add more keys, but not under an existing keyid. That can +# be done by any of his/her identities (here we choose u4). +try " + ADDNOK u5 u1 \@second; /FATAL: keyid already in use: \@second/ + ADD u5 u4 \@third > session; ok + cat session | ssh u5 ukm; ok; /pending keyid added: \@third/ +"; + +# u2 cannot add the same key, but is allowed to use the same name (@third). +try " + ADDNOK u5 u2 \@third; /FATAL: cannot add key/ + /Same key is already available under another userid./ + ADD u6 u2 \@third > session; ok + cat session | ssh u6 ukm; ok; /pending keyid added: \@third/ +"; + +# u6 can schedule his/her own key for deletion, but cannot actually +# remove it. Trying to do so results in bringing back the key. Actual +# deletion must be confirmed by another key. +try " + ssh u6 ukm del \@third; /prepare deletion of key \@third/ + ssh u2 ukm; ok; /u2 \@third .pending del./ + ssh u6 ukm; ok; /undo pending deletion of keyid \@third/ + ssh u6 ukm del \@third; /prepare deletion of key \@third/ + ssh u2 ukm del \@third; ok; /pending keyid deleted: \@third/ +"; + +# While in pending-deletion state, it's forbidden to add another key +# with the same keyid. It's also forbidden to add a key with the same +# fingerprint as the to-be-deleted key). +# A new key under another keyid, is OK. +try " + ssh u1 ukm del \@third; /prepare deletion of key \@third/ + ADDNOK u4 u1 \@third; /FATAL: keyid already in use: \@third/ + ADDNOK u5 u1 \@fourth; + /FATAL: You cannot add a key that already belongs to you./ + ADD u6 u1 \@fourth > session; ok + ssh u1 ukm; ok; + /u1 \@second/ + /u1 \@fourth .pending add./ + /u1 \@third .pending del./ +"; +# We can remove a pending-for-addition key (@fourth) by logging in +# with a non-pending key. Trying to do anything with key u5 (@third) +# will just bring it back to its normal state, but not change the +# state of any other key. As already shown above, using u6 (@fourth) +# without a proper session key, would remove it from the system. +# Here we want to demonstrate that key u1 can delete u6 immediately. +try "ssh u1 ukm del \@fourth; /pending keyid deleted: \@fourth/"; + +# The pending-for-deletion key @third can also be removed via the u4 +# (@second) key. +try "ssh u4 ukm del \@third; ok; /pending keyid deleted: \@third/"; + +# Non-existing selfkeys cannot be deleted. +try "ssh u4 ukm del \@x; !ok; /FATAL: You are not managing the key \@x./"; |