diff options
author | Sergey Poznyakoff <gray@gnu.org> | 2017-09-27 16:29:50 +0200 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org> | 2017-09-27 16:29:50 +0200 |
commit | ee07987c9da7a4d349b92df8025665ce140b7cba (patch) | |
tree | 05054fc05f3ae5cb81bcc0469de009e6ce0791fc | |
parent | 9b6e0bcb9b86c14456e6fabeac4e396086bf8485 (diff) | |
download | sourceyard-ee07987c9da7a4d349b92df8025665ce140b7cba.tar.gz sourceyard-ee07987c9da7a4d349b92df8025665ce140b7cba.tar.bz2 |
Implement editing the SSH and GPG keys
-rw-r--r-- | lib/App/Sourceyard/GPG/Key.pm | 116 | ||||
-rw-r--r-- | lib/App/Sourceyard/GPG/PublicKey.pm | 97 | ||||
-rw-r--r-- | lib/Sourceyard.pm | 5 | ||||
-rw-r--r-- | lib/Sourceyard/Controller/User.pm | 77 | ||||
-rw-r--r-- | lib/Sourceyard/Schema/Result/GPG_Keys.pm | 13 | ||||
-rw-r--r-- | lib/Sourceyard/User.pm | 31 | ||||
-rw-r--r-- | public/css/internal/base.css | 21 | ||||
-rw-r--r-- | public/images/.gitignore | 3 | ||||
-rw-r--r-- | public/images/common/bool1/ok.orig.png (renamed from public/images/common/bool1/ok.png) | bin | 2135 -> 2135 bytes | |||
-rw-r--r-- | public/images/common/bool1/wrong.orig.png (renamed from public/images/common/bool1/wrong.png) | bin | 2512 -> 2512 bytes | |||
-rw-r--r-- | templates/user/admin.html.ep | 27 | ||||
-rw-r--r-- | templates/user/gpg_keys.html.ep | 53 | ||||
-rw-r--r-- | templates/user/ssh_keys.html.ep | 38 |
13 files changed, 460 insertions, 21 deletions
diff --git a/lib/App/Sourceyard/GPG/Key.pm b/lib/App/Sourceyard/GPG/Key.pm new file mode 100644 index 0000000..7bfb337 --- /dev/null +++ b/lib/App/Sourceyard/GPG/Key.pm @@ -0,0 +1,116 @@ +package App::Sourceyard::GPG::Key; + +use strict; +use warnings; +use Carp; +use parent 'Exporter'; +use DateTime; + +my @ATTRIBUTES = qw(type + validity + length + algo + keyid + creation_date + expiry_date + serial + ownertrust + uid + sigclass + capa); + +my %ATTRIBTYPE = (uid => 'ARRAY'); + +{ + no strict 'refs'; + foreach my $attribute (@ATTRIBUTES) { + if ($ATTRIBTYPE{$attribute} && $ATTRIBTYPE{$attribute} eq 'ARRAY') { + *{ __PACKAGE__ . '::' . $attribute } = sub { + my $self = shift; + @{$self->{$attribute}} = @_ if @_; + return @{$self->{$attribute}}; + }; + } else { + *{ __PACKAGE__ . '::' . $attribute } = sub { + my $self = shift; + $self->{$attribute} = shift if @_; + return $self->{$attribute}; + } + }; + } +} + +sub algoname { + my ($self) = @_; + my %trans = ( + 1 => 'RSA', + 16 => 'Elgamal (encrypt only)', + 17 => 'DSA (sign only)', + 20 => 'Elgamal'); + my $c = $self->algo; + return $trans{$c} || $c; +} + +sub _convdate { + my $datestr = shift; + + return undef unless defined $datestr; + + if ($datestr =~ /^(?<Y>\d{4}) + (?<m>\d{2}) + (?<d>\d{2}) + T + (?<H>\d{2}) + (?<M>\d{2}) + (?<S>\d{2})$/x) { + return new DateTime(year => $+{Y}, + month => $+{m}, + day => $+{d}, + hour => $+{H}, + minute => $+{M}, + second => $+{S}); + } else { + return DateTime->from_epoch(epoch => $datestr); + } +} + +sub new { + my $class = shift; + my %info = (); + @info{@ATTRIBUTES} = map { defined($_) && $_ eq '' ? undef : $_ } @_; + $info{uid} = [ $info{uid} ] if $info{uid}; + $info{creation_date} = _convdate($info{creation_date}); + $info{expiry_date} = _convdate($info{expiry_date}); + return bless \%info, $class; +} + +sub fingerprint { + my ($self, $fpr) = @_; + if ($fpr) { + $self->{fingerprint} = $fpr; + } + return $self->{fingerprint}; +} + +sub addsubkey { + my ($self, $sk) = @_; + push @{$self->{subkeys}}, $sk; +} + +sub subkey { + my ($self, $n) = @_; + $n ||= 0; + if ($n < 0) { + $n += @{$self->{subkeys}}; + return undef if $n < 0; + } + return undef if $n > $#{$self->{subkeys}}; + return $self->{subkeys}[$n]; +} + +sub adduid { + my ($self, $id) = @_; + push @{$self->{uid}}, $id; +} + +1; diff --git a/lib/App/Sourceyard/GPG/PublicKey.pm b/lib/App/Sourceyard/GPG/PublicKey.pm new file mode 100644 index 0000000..3e3ef2d --- /dev/null +++ b/lib/App/Sourceyard/GPG/PublicKey.pm @@ -0,0 +1,97 @@ +package App::Sourceyard::GPG::PublicKey; +use strict; +use warnings; +use Carp; +use parent 'Exporter'; +use POSIX::Run::Capture; +use App::Sourceyard::GPG::Key; + +our $gpgbin = 'gpg'; + +sub new { + my ($class, $key) = @_; + my $self = bless {}, $class; + if ($key) { + $self->blob($key); + } + return $self; +} + +sub _parse { + my ($self, $blob) = @_; + my $out; + my $cap = new POSIX::Run::Capture( + argv => [ $gpgbin, + '--with-colons', + '--with-fingerprint', + '--with-fingerprint', + '--fixed-list-mode' ], + stdin => $blob, + # FIXME: stderr => log + timeout => 5 + ); + unless ($cap->run && $cap->status == 0) { + return undef; + } + + while ($_ = $cap->next_line(1)) { + chomp; + my @fields = split /:/; + my $type = shift @fields; + if ($type eq 'pub') { + push @{$self->{_pubkeys}}, + new App::Sourceyard::GPG::Key($type, @fields); + } elsif ($type eq 'sub') { + $self->pubkey(-1)->addsubkey( + new App::Sourceyard::GPG::Key($type, @fields) + ); + } elsif ($type eq 'fpr') { + if ($self->pubkey(-1)->subkey) { + $self->pubkey(-1)->subkey(-1)->fingerprint($fields[8]); + } else { + $self->pubkey(-1)->fingerprint($fields[8]); + } + } elsif ($type eq 'uid') { + if ($self->pubkey(-1)->subkey) { + $self->pubkey(-1)->subkey(-1)->adduid($fields[8]); + } else { + $self->pubkey(-1)->adduid($fields[8]); + } + } + } + + return @{$self->{_pubkeys}}; +} + +sub blob { + my ($self, $newblob) = @_; + if ($newblob) { + delete $self->{_pubkey}; + if ($self->_parse($newblob)) { + $self->{_blob} = $newblob; + } else { + croak "invalid key"; + } + } + return $self->{_blob}; +} + +sub pubkey { + my ($self, $n) = @_; + $n ||= 0; + if ($n < 0) { + $n += @{$self->{_pubkeys}}; + return undef if $n < 0; + } + return undef if $n > $#{$self->{_pubkeys}}; + + return $self->{_pubkeys}[$n]; +} + +sub pubkeys { + my ($self) = @_; + return () unless exists $self->{_pubkeys}; + return @{$self->{_pubkeys}}; +} + +1; diff --git a/lib/Sourceyard.pm b/lib/Sourceyard.pm index 34d67c8..74a1c0e 100644 --- a/lib/Sourceyard.pm +++ b/lib/Sourceyard.pm @@ -61,8 +61,9 @@ sub startup { } return 1; }); - $auth->get(':action')->to(controller => 'user'); - $auth->post(':action')->to(controller => 'user'); + $auth->any(['GET', 'POST'] => '/:action' => + [action => [ qw(admin ssh_keys gpg_keys) ]])->to(controller => 'user'); +# $auth->post(':action')->to(controller => 'user'); } sub _top_menu_item { diff --git a/lib/Sourceyard/Controller/User.pm b/lib/Sourceyard/Controller/User.pm index 0ec562d..7f0ba19 100644 --- a/lib/Sourceyard/Controller/User.pm +++ b/lib/Sourceyard/Controller/User.pm @@ -81,4 +81,81 @@ sub admin { $self->render; } +sub ssh_keys { + my $self = shift; + if ($self->req->method eq 'POST') { + my $user = $self->stash('user'); + $self->{_sy_update} = 0; + my $i; + foreach my $k ($user->authorized_keys->all) { + $i++; + if ($self->param("del_$i")) { + $self->db->resultset('Authorized_Keys') + ->find({ key_id => $k->key_id })->delete; + $self->{_sy_update} = 1; + last; + } + my $s = $self->param("key_$i"); # FIXME: Normalize + if ($s ne $k->ssh_key) { + if ($s = $user->validate_ssh_key($s)) { + $self->db->resultset('Authorized_Keys') + ->find({ key_id => $k->key_id })->ssh_key($s); + $self->{_sy_update} = 1; + } else { + $self->stash('error_msg', "Invalid SSH key #$i"); + } + last; + } + } + + if (my $s = $self->param("key_new")) { + $i++; + if ($s = $user->validate_ssh_key($s)) { + $self->db->resultset('Authorized_Keys') + ->create({ + user_id => $user->user_id, + ssh_key => $s + }); + $self->{_sy_update} = 1; + $self->param(key_new => undef); + } else { + $self->stash('error_msg', "Invalid SSH key #$i"); + } + } + } + $self->render; +} + +sub gpg_keys { + my $self = shift; + if ($self->req->method eq 'POST') { + my $user = $self->stash('user'); + $self->{_sy_update} = 0; + my $i; + foreach my $k ($user->gpg_keys->all) { + $i++; + if ($self->param("del_$i")) { + $self->db->resultset('GPG_Keys') + ->find({ key_id => $k->key_id })->delete; + $self->{_sy_update} = 1; + last; + } + } + if (my $s = $self->param("key_new")) { + if ($s = $user->validate_gpg_key($s)) { + $self->db->resultset('GPG_Keys') + ->create({ + user_id => $user->user_id, + gpg_key => $s->blob + }); + $self->{_sy_update} = 1; + } else { + $self->stash('error_msg', "Invalid GPG key"); + } + $self->param(key_new => undef); + } + } + $self->render; +} + 1; diff --git a/lib/Sourceyard/Schema/Result/GPG_Keys.pm b/lib/Sourceyard/Schema/Result/GPG_Keys.pm index 9416b31..8699040 100644 --- a/lib/Sourceyard/Schema/Result/GPG_Keys.pm +++ b/lib/Sourceyard/Schema/Result/GPG_Keys.pm @@ -4,6 +4,7 @@ use strict; use warnings; use base 'DBIx::Class::Core'; +use App::Sourceyard::GPG::PublicKey; __PACKAGE__->table('gpg_keys'); __PACKAGE__->add_columns( @@ -21,4 +22,16 @@ __PACKAGE__->add_columns( __PACKAGE__->set_primary_key('key_id'); __PACKAGE__->belongs_to('user_id' => 'Sourceyard::Schema::Result::User'); +__PACKAGE__->inflate_column('gpg_key', { + inflate => sub { + my ($value, $object) = @_; + return new App::Sourceyard::GPG::PublicKey($value); + }, + deflate => sub { + my ($value, $object) = @_; + return $value->blob; + } +}); + + 1; diff --git a/lib/Sourceyard/User.pm b/lib/Sourceyard/User.pm index f5ca31e..cc3524e 100644 --- a/lib/Sourceyard/User.pm +++ b/lib/Sourceyard/User.pm @@ -6,6 +6,7 @@ use Email::Address::XS; use Net::DNS; use Crypt::Cracklib; use App::Sourceyard::Password; +use Net::SSH::Perl::Key; use parent qw(Sourceyard::Schema::Result::User); #use parent qw(DBIx::Class::ResultSet); @@ -65,4 +66,34 @@ sub validate_password { return new App::Sourceyard::Password($pass); } +sub validate_ssh_key { + my ($self, $key) = @_; + + $key =~ s/\n//; + $key =~ s/^\s+//; + $key =~ s/\s+$//; + + my @in = split /\s+/, $key; + my $type = shift @in; + my @block; + while (my $frag = shift @in) { + if ($frag =~ m{^[A-Za-z0-9+/=]+$}) { + push @block, $frag; + $key = $type . ' ' . join('', @block) . ' ' . join(' ', @in); + return $key if + eval { Net::SSH::Perl::Key->extract_public(undef, $key) }; + } + } + + return undef; +} + +sub validate_gpg_key { + my ($self, $key) = @_; + my $pk; + eval { + $pk = new App::Sourceyard::GPG::PublicKey($key); + }; + return $pk; +} 1; diff --git a/public/css/internal/base.css b/public/css/internal/base.css index 27e714c..906ad78 100644 --- a/public/css/internal/base.css +++ b/public/css/internal/base.css @@ -886,7 +886,14 @@ div.input input { div.input select { display: table-cell; } - +div.input textarea { + display: table-cell; +} + +*.cell { + display: table-cell; +} + button { border: 1px solid gray; background-color: white; @@ -904,3 +911,15 @@ button.cancel { color: white; } +table.input { + border-collapse: collapse; +} +table.input td { + padding-right: 2em; +} +table.input thead td { + font-weight: bold; + text-align: center; + border-bottom: 1px solid gray; +} +
\ No newline at end of file diff --git a/public/images/.gitignore b/public/images/.gitignore new file mode 100644 index 0000000..2bdfe67 --- /dev/null +++ b/public/images/.gitignore @@ -0,0 +1,3 @@ +stamp-icons +*.png +! *.orig.png diff --git a/public/images/common/bool1/ok.png b/public/images/common/bool1/ok.orig.png Binary files differindex ca7034e..ca7034e 100644 --- a/public/images/common/bool1/ok.png +++ b/public/images/common/bool1/ok.orig.png diff --git a/public/images/common/bool1/wrong.png b/public/images/common/bool1/wrong.orig.png Binary files differindex bca36a5..bca36a5 100644 --- a/public/images/common/bool1/wrong.png +++ b/public/images/common/bool1/wrong.orig.png diff --git a/templates/user/admin.html.ep b/templates/user/admin.html.ep index 8f5f6f7..0f9103b 100644 --- a/templates/user/admin.html.ep +++ b/templates/user/admin.html.ep @@ -28,25 +28,16 @@ </p> % end %= alt_tag div => (class => 'boxitem') => begin - <a href="editsshkeys.php"> - Register an SSH Public Key + <a href="ssh_keys"> + Register SSH Public Keys </a> -% foreach my $k ($user->authorized_keys->all) { - <p class="smaller"> - <div style="width: 20em; height: 2em; overflow-x: scroll; - overflow-y: hidden;"> - <%= $k->ssh_key %> - </div> -%# %= text_field k => $k->ssh_key, size => 40, disabled => 'disabled' -%# %= substr($k->ssh_key ,0, 10) - </p> -% end -%# Usually, SSH Keys can be used to get secure shell access, or to -%# run rsync or cvs commands. -%# </p> -% end + <p class="smaller"> + Usually, SSH Keys can be used to get secure shell access, or to + run rsync or cvs commands. + </p> +% end %= alt_tag div => (class => 'boxitem') => begin - <a href="change.php?item=gpgkey"> + <a href="gpg_keys"> Edit GPG Key </a> <p class="smaller"> @@ -108,7 +99,7 @@ %= opt_input 'mail', 'email' => $user->email % end %= alt_tag div => (class => 'boxitem') => begin - <a href="change_notifications"> + <a href="notifications"> Edit Personal Notification Settings </a> <p class="smaller"> diff --git a/templates/user/gpg_keys.html.ep b/templates/user/gpg_keys.html.ep new file mode 100644 index 0000000..0b01a16 --- /dev/null +++ b/templates/user/gpg_keys.html.ep @@ -0,0 +1,53 @@ +% layout 'user'; +% title 'My GPG keys'; +%= include "include/feedback" +<h3>Registered GPG keys</h3> +<div class="form"> +%= form_for 'gpg_keys' => (method => 'POST') => begin +% my $i = 0; +% foreach my $k ($user->gpg_keys->all) { +% $i++; + <table class="input"> + <thead> + <tr> + <td>Key ID</td> + <td>Fingerprint</td> + <td>User ID</td> + <td>Lenght & algorithm</td> + </tr> + </thead> + <tbody> + <tr> + <td> + <%= $k->gpg_key->pubkey->keyid %> + </td> + <td> + <%= $k->gpg_key->pubkey->fingerprint %> + </td> + <td> + <%= ${[$k->gpg_key->pubkey->uid]}[0] %> + </td> + <td> + <span class="smaller"> + (<%= $k->gpg_key->pubkey->length %> bits, + <%= $k->gpg_key->pubkey->algoname %>)</span> + </td> + <td> +%# %= text_field "key_$i" => $k->gpg_key->blob, size => '100' +%= tag 'button' => (class => 'cancel', name => "del_$i", value => 1) => begin + x +% end + </td> + </tbody> + </table> +% } + <h3>Add new key</h3> + <div class="input"> +%= text_area "key_new", cols => 70, rows => 10, wrap => 'virtual' + <div class="center"> + %= submit_button 'Add' + </div> + </div> +% end +</div> + diff --git a/templates/user/ssh_keys.html.ep b/templates/user/ssh_keys.html.ep new file mode 100644 index 0000000..46c2252 --- /dev/null +++ b/templates/user/ssh_keys.html.ep @@ -0,0 +1,38 @@ +% layout 'user'; +% title 'My SSH keys'; +%= include "include/feedback" +<div class="form"> +%= form_for 'ssh_keys' => (method => 'POST') => begin +% use Net::SSH::Perl::Key; +% my $i = 0; +% foreach my $k ($user->authorized_keys->all) { +% $i++; +% my ($size, $fingerprint); +% if (my $x = eval { Net::SSH::Perl::Key->extract_public(undef,$k->ssh_key) }) { +% $size = $x->size; +% $fingerprint = $x->fingerprint('md5'); +% } +<div class="input"> +%= label_for "key_$i" => "Key #$i:", class => 'preinput' +%# %= text_area "key_$i" => $k->ssh_key, cols => 80, rows => 10, wrap => 'hard' +%= text_field "key_$i" => $k->ssh_key, size => '100' +%= tag 'button' => (class => 'cancel', name => "del_$i", value => 1) => begin + x +% end +</div> +<div class="input"> + %= label_for 'a' => 'size fingerprint', class => 'preinput smaller' + <span class="cell smaller"><%= $size %> <%= $fingerprint %></span> +</div> +% } +<div class="input"> +% $i++; +%= label_for "key_new" => "Key #$i (new):", class => 'preinput' +%= text_field "key_new" => '', size => '100' +%# %= file_field "file_$i" +%= tag 'button' => (class => 'update', name => 'key_new_add', value => 1) => begin + + +% end +</div> + +% end |