From 938ea5aede414411dee30a85720430fa72ba3305 Mon Sep 17 00:00:00 2001 From: Sam Batschelet Date: Sat, 17 Jun 2017 17:46:08 -0400 Subject: [PATCH] Add token based authentication via grpc-gateway with tests. Fixes GH #18 (#22) --- .travis.yml | 7 +- lib/Net/Etcd.pm | 107 +++++++++++++++++++----------- lib/Net/Etcd/Auth.pm | 46 ++++++++++--- lib/Net/Etcd/Auth/Role.pm | 2 +- lib/Net/Etcd/Role/Actions.pm | 11 ++- t/{key_value.t => 01-key_value.t} | 0 t/{lease.t => 02-lease.t} | 0 t/{maint.t => 03-maint.t} | 0 t/{txn.t => 04-txn.t} | 0 t/{user.t => 05-user.t} | 2 +- t/{watch.t => 05-watch.t} | 2 +- t/99-auth.t | 80 ++++++++++++++++++++++ 12 files changed, 199 insertions(+), 58 deletions(-) rename t/{key_value.t => 01-key_value.t} (100%) rename t/{lease.t => 02-lease.t} (100%) rename t/{maint.t => 03-maint.t} (100%) rename t/{txn.t => 04-txn.t} (100%) rename t/{user.t => 05-user.t} (95%) rename t/{watch.t => 05-watch.t} (95%) create mode 100644 t/99-auth.t diff --git a/.travis.yml b/.travis.yml index 71b9ab0..024d652 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ sudo: required env: global: - - ETCD_VER=v3.2.0 + - ETCD_VER=v3.2.0_plus_git - ETCDCTL_API=3 - ETCD_TEST_HOST=127.0.0.1 - ETCD_TEST_PORT=2379 @@ -23,9 +23,10 @@ matrix: env: COVERAGE=1 # enables coverage+coveralls reporting before_install: - curl https://coreos.com/dist/pubkeys/app-signing-pubkey.gpg | sudo apt-key add - - - wget https://github.com/coreos/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -O /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz + - wget https://github.com/hexfusion/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -O /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz - mkdir -p /tmp/test-etcd - tar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /tmp/test-etcd --strip-components=1 + - /tmp/test-etcd/etcd -version - /tmp/test-etcd/etcd& - sleep 3 - git clone git://github.com/travis-perl/helpers ~/travis-perl-helpers @@ -40,4 +41,4 @@ install: script: - perl Makefile.PL - make - - prove -b -r -s -j$(test-jobs) $(test-files) + - make test diff --git a/lib/Net/Etcd.pm b/lib/Net/Etcd.pm index c31d60c..929ae85 100644 --- a/lib/Net/Etcd.pm +++ b/lib/Net/Etcd.pm @@ -65,6 +65,15 @@ our $VERSION = '0.011'; # attach lease to put $etcd->put( { key => 'foo2', value => 'bar2', lease => 7587821338341002662 } ); + # add new user + $etcd->user( { name => 'samba', password => 'foo' } )->add; + + # add new user role + $role = $etcd->role( { name => 'myrole' } )->add; + + # grant role + $etcd->user_role( { user => 'samba', role => 'myrole' } )->grant; + =head1 DESCRIPTION L is object oriented interface to the v3 REST API provided by the etcd L. @@ -74,6 +83,8 @@ L is object oriented interface to the v3 REST API provided by the etc =head2 host +The etcd host. Defaults to 127.0.0.1 + =cut has host => ( @@ -84,6 +95,8 @@ has host => ( =head2 port +Default 2379. + =cut has port => ( @@ -92,7 +105,9 @@ has port => ( default => '2379' ); -=head2 username +=head2 name + +Username for authentication =cut @@ -103,6 +118,8 @@ has name => ( =head2 password +Authentication credentials + =cut has password => ( @@ -112,6 +129,8 @@ has password => ( =head2 ssl +To enable set to 1 + =cut has ssl => ( @@ -119,27 +138,13 @@ has ssl => ( isa => Bool, ); -=head2 api_root - -=cut - -has api_root => ( is => 'lazy' ); - -sub _build_api_root { - my ($self) = @_; - return - ( $self->ssl ? 'https' : 'http' ) . '://' - . $self->host . ':' - . $self->port; -} - -=head2 api_prefix +=head2 api_version defaults to /v3alpha =cut -has api_prefix => ( +has api_version => ( is => 'ro', isa => Str, default => '/v3alpha' @@ -147,34 +152,32 @@ has api_prefix => ( =head2 api_path +The full api path. Defaults to http://127.0.0.1:2379/v3alpha + =cut has api_path => ( is => 'lazy' ); sub _build_api_path { my ($self) = @_; - return $self->api_root . $self->api_prefix; + return ( $self->ssl ? 'https' : 'http' ) . '://' + . $self->host . ':'. $self->port . $self->api_version; } =head2 auth_token -=cut +The token that is passed during authentication. This is generated during the +authentication process and stored until no longer valid or username is changed. -has auth_token => ( is => 'lazy' ); +=cut -sub _build_auth_token { - my ($self) = @_; - return Net::Etcd::Auth::Authenticate->new( - etcd => $self, - %$self - )->token; -} +has auth_token => ( is => 'rwp' ); =head1 PUBLIC METHODS =head2 watch -Returns a L object. +See L $etcd->watch({ key =>'foo', range_end => 'fop' }) @@ -192,7 +195,7 @@ sub watch { =head2 role -Returns a L object. +See L $etcd->role({ role => 'foo' }); @@ -210,7 +213,7 @@ sub role { =head2 user_role -Returns a L object. +See L $etcd->user_role({ name => 'samba', role => 'foo' }); @@ -228,7 +231,11 @@ sub user_role { =head2 auth -Returns a L object. +See L + + $etcd->auth({ name => 'samba', password => 'foo' })->authenticate; + $etcd->auth()->enable; + $etcd->auth()->disable =cut @@ -244,7 +251,9 @@ sub auth { =head2 lease -Returns a L object. +See L + + $etcd->lease( { ID => 7587821338341002662, TTL => 20 } )->grant; =cut @@ -260,7 +269,9 @@ sub lease { =head2 maintenance -Returns a L object. +See L + + $etcd->maintenance()->snapshot =cut @@ -276,7 +287,9 @@ sub maintenance { =head2 user -Returns a L object. +See L + + $etcd->user( { name => 'samba', password => 'foo' } )->add; =cut @@ -292,31 +305,43 @@ sub user { =head2 put -Returns a L object. +See L + + $etcd->put({ key =>'foo1', value => 'bar' }); =cut =head2 range -Returns a L object. +See L + + $etcd->range({ key =>'test0', range_end => 'test100' }); =cut =head2 txn -Returns a L object. +See L + + $etcd->txn({ compare => \@compare, success => \@op }); =cut =head2 op -Returns a L object. +See L + + $etcd->op({ request_put => $put }); + $etcd->op({ request_delete_range => $range }); =cut =head2 compare -Returns a L object. +See L + + $etcd->compare( { key => 'foo', result => 'EQUAL', target => 'VALUE', value => 'baz' }); + $etcd->compare( { key => 'foo', target => 'CREATE', result => 'NOT_EQUAL', create_revision => '2' }); =cut @@ -337,6 +362,8 @@ sub BUILD { $msg .= ">> Please install etcd - https://coreos.com/etcd/docs/latest/"; die $msg; } + # set the intial auth token + $self->auth()->authenticate; } =head1 AUTHOR @@ -350,7 +377,7 @@ The L developers and community. =head1 CAVEATS The L v3 API is in heavy development and can change at anytime please see -https://github.com/coreos/etcd/blob/master/Documentation/dev-guide/api_reference_v3.md + L for latest details. =head1 LICENSE AND COPYRIGHT diff --git a/lib/Net/Etcd/Auth.pm b/lib/Net/Etcd/Auth.pm index e4ef600..0df5746 100644 --- a/lib/Net/Etcd/Auth.pm +++ b/lib/Net/Etcd/Auth.pm @@ -9,9 +9,11 @@ use warnings; =cut use Moo; +use JSON; use Carp; use Types::Standard qw(Str Int Bool HashRef ArrayRef); use Net::Etcd::Auth::Role; +use Data::Dumper; with 'Net::Etcd::Role::Actions'; @@ -57,41 +59,60 @@ has endpoint => ( isa => Str, ); -=head2 password +=head2 name + +Defaults to $etcd->name =cut has name => ( - is => 'ro', - isa => Str, + is => 'lazy', ); +sub _build_name { + my ($self) = @_; + my $user = $self->etcd->name; + return $user if $user; + return; +} + =head2 password +Defaults to $etcd->password + =cut has password => ( - is => 'ro', - isa => Str, + is => 'lazy', ); +sub _build_password { + my ($self) = @_; + my $pwd = $self->etcd->password; + return $pwd if $pwd; + return; +} + =head1 PUBLIC METHODS =head2 authenticate -Enable authentication, this requires name and password. +Returns token with valid authentication. - $etcd->auth({ name => $user, password => $pass })->authenticate; + my $token = $etcd->auth({ name => $user, password => $pass })->authenticate; =cut sub authenticate { my ( $self, $options ) = @_; $self->{endpoint} = '/auth/authenticate'; - confess 'name and password required for ' . __PACKAGE__ . '->authenticate' - unless ($self->{password} && $self->{name}); + return unless ($self->password && $self->name); $self->request; - return $self; + my $auth = from_json($self->{response}{content}); + if ($auth && defined $auth->{token}) { + $self->etcd->{auth_token} = $auth->{token}; + } + return; } =head2 enable @@ -107,6 +128,9 @@ sub enable { $self->{endpoint} = '/auth/enable'; $self->{json_args} = '{}'; $self->request; + + # init token + $self->etcd->auth()->authenticate; return $self; } @@ -122,7 +146,7 @@ sub disable { my ( $self, $options ) = @_; $self->{endpoint} = '/auth/disable'; confess 'root name and password required for ' . __PACKAGE__ . '->disable' - unless ($self->{password} && $self->{name}); + unless ($self->password && $self->name); $self->request; return $self; } diff --git a/lib/Net/Etcd/Auth/Role.pm b/lib/Net/Etcd/Auth/Role.pm index d956343..2b06a3a 100644 --- a/lib/Net/Etcd/Auth/Role.pm +++ b/lib/Net/Etcd/Auth/Role.pm @@ -80,7 +80,7 @@ Delete role sub delete { my ($self) = @_; - confess 'name required for ' . __PACKAGE__ . '->delete' + confess 'role required for ' . __PACKAGE__ . '->delete' unless $self->{role}; $self->{endpoint} = '/auth/role/delete'; $self->request; diff --git a/lib/Net/Etcd/Role/Actions.pm b/lib/Net/Etcd/Role/Actions.pm index fc7a958..b5ed341 100644 --- a/lib/Net/Etcd/Role/Actions.pm +++ b/lib/Net/Etcd/Role/Actions.pm @@ -84,8 +84,16 @@ sub init { =cut -has headers => ( is => 'ro' ); +has headers => ( is => 'lazy' ); +sub _build_headers { + my ($self) = @_; + my $headers; + my $token = $self->etcd->auth_token; + $headers->{'Content-Type'} = 'application/json'; + $headers->{'Authorization'} = $token if $token; + return $headers; +} =head2 hold When set will not fire request. @@ -113,6 +121,7 @@ sub _build_request { my $cb = $self->cb; my $cv = $self->cv ? $self->cv : AE::cv; $cv->begin; + http_request( 'POST', $self->etcd->api_path . $self->{endpoint}, diff --git a/t/key_value.t b/t/01-key_value.t similarity index 100% rename from t/key_value.t rename to t/01-key_value.t diff --git a/t/lease.t b/t/02-lease.t similarity index 100% rename from t/lease.t rename to t/02-lease.t diff --git a/t/maint.t b/t/03-maint.t similarity index 100% rename from t/maint.t rename to t/03-maint.t diff --git a/t/txn.t b/t/04-txn.t similarity index 100% rename from t/txn.t rename to t/04-txn.t diff --git a/t/user.t b/t/05-user.t similarity index 95% rename from t/user.t rename to t/05-user.t index 295dcf5..b275d94 100644 --- a/t/user.t +++ b/t/05-user.t @@ -20,7 +20,7 @@ else { plan skip_all => "Please set environment variable ETCD_TEST_HOST and ETCD_TEST_PORT."; } -my $etcd = Net::Etcd->new( { host => $host, port => $port } ); +my $etcd = Net::Etcd->new( { host => $host, port => $port, name => 'root', password => 'toor' } ); my ($user, $role); diff --git a/t/watch.t b/t/05-watch.t similarity index 95% rename from t/watch.t rename to t/05-watch.t index fcc0adf..104e102 100644 --- a/t/watch.t +++ b/t/05-watch.t @@ -18,7 +18,7 @@ else { } my ($watch,$key); -my $etcd = Net::Etcd->new( { host => $host, port => $port } ); +my $etcd = Net::Etcd->new( { host => $host, port => $port} ); our @events; # create watch with callback and store events diff --git a/t/99-auth.t b/t/99-auth.t new file mode 100644 index 0000000..32f7f02 --- /dev/null +++ b/t/99-auth.t @@ -0,0 +1,80 @@ +#!perl + +use strict; +use warnings; + +use Net::Etcd; +use Test::More; +use Test::Exception; +use Data::Dumper; + +my ($host, $port); + +if ( $ENV{ETCD_TEST_HOST} and $ENV{ETCD_TEST_PORT}) { + $host = $ENV{ETCD_TEST_HOST}; + $port = $ENV{ETCD_TEST_PORT}; + + plan tests => 8; +} +else { + plan skip_all => "Please set environment variable ETCD_TEST_HOST and ETCD_TEST_PORT."; +} + +my $etcd = Net::Etcd->new( { host => $host, port => $port, name => 'root', password => 'toor' } ); + +my ($user, $role, $auth); + +# add user +lives_ok( + sub { + $user = + $etcd->user( { name => 'root', password => 'toor' } )->add; + }, + "add a new user" +); + +#print STDERR Dumper($user); + +# add new role +lives_ok( sub { $role = $etcd->role( { name => 'root' } )->add; + }, + "add a new role" ); + +#print STDERR Dumper($role); + +# grant role +lives_ok( + sub { + $role = + $etcd->user_role( { user => 'root', role => 'root' } )->grant; + }, + "grant role" +); + +#print STDERR Dumper($role); + +cmp_ok( $role->{response}{success}, '==', 1, "grant role success" ); + +# enable auth +lives_ok( + sub { + $auth = + $etcd->auth()->enable; + }, + "enable auth" +); + +cmp_ok( $auth->{response}{success}, '==', 1, "enable auth" ); + +# disable auth +lives_ok( + sub { + $auth = + $etcd->auth()->disable; + }, + "disable auth" +); + +cmp_ok( $auth->{response}{success}, '==', 1, "disable auth" ); + +1;