diff --git a/lib/NGCP/Panel/Controller/API/Admins.pm b/lib/NGCP/Panel/Controller/API/Admins.pm index df5d545d99..b4b375b7e5 100644 --- a/lib/NGCP/Panel/Controller/API/Admins.pm +++ b/lib/NGCP/Panel/Controller/API/Admins.pm @@ -3,17 +3,11 @@ use NGCP::Panel::Utils::Generic qw(:all); use Sipwise::Base; -use boolean qw(true); -use Data::HAL qw(); -use Data::HAL::Link qw(); -use HTTP::Headers qw(); +use parent qw/NGCP::Panel::Role::Entities NGCP::Panel::Role::API::Admins/; + use HTTP::Status qw(:constants); -use NGCP::Panel::Utils::Admin; -require Catalyst::ActionRole::ACL; -require Catalyst::ActionRole::CheckTrailingSlash; -require NGCP::Panel::Role::HTTPMethods; -require Catalyst::ActionRole::RequireSSL; +__PACKAGE__->set_config(); sub api_description { return'Defines admins to log into the system via panel or api.'; @@ -27,197 +21,31 @@ sub query_params { { param => 'reseller_id', description => 'Filter for admins belonging to a specific reseller', - query => { - first => sub { - my $q = shift; - { reseller_id => $q }; - }, - second => sub {}, - }, + query_type => 'string_eq', }, { param => 'login', description => 'Filter for admins with a specific login (wildcards possible)', - query => { - first => sub { - my $q = shift; - { login => { like => $q } }; - }, - second => sub {}, - }, + query_type => 'string_like', }, ]; } -use parent qw/Catalyst::Controller NGCP::Panel::Role::API::Admins/; - -sub resource_name{ - return 'admins'; -} -sub dispatch_path{ - return '/api/admins/'; -} -sub relation{ - return 'http://purl.org/sipwise/ngcp-api/#rel-admins'; -} - -__PACKAGE__->config( - action => { - map { $_ => { - ACLDetachTo => '/api/root/invalid_user', - AllowedRole => [qw/admin reseller/], - Args => 0, - Does => [qw(ACL CheckTrailingSlash RequireSSL)], - Method => $_, - Path => __PACKAGE__->dispatch_path, - } } @{ __PACKAGE__->allowed_methods } - }, -); - -sub gather_default_action_roles { - my ($self, %args) = @_; my @roles = (); - push @roles, 'NGCP::Panel::Role::HTTPMethods' if $args{attributes}->{Method}; - return @roles; -} - -sub auto :Private { - my ($self, $c) = @_; - - $self->set_body($c); - $self->log_request($c); -} - -sub GET :Allow { - my ($self, $c) = @_; - my $page = $c->request->params->{page} // 1; - my $rows = $c->request->params->{rows} // 10; - { - my $items = $self->item_rs($c); - (my $total_count, $items) = $self->paginate_order_collection($c, $items); - my (@embedded, @links); - my $form = $self->get_form($c); - for my $item ($items->all) { - push @embedded, $self->hal_from_item($c, $item, $form); - push @links, Data::HAL::Link->new( - relation => 'ngcp:'.$self->resource_name, - href => sprintf('/%s%d', $c->request->path, $item->id), - ); - } - push @links, - Data::HAL::Link->new( - relation => 'curies', - href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', - name => 'ngcp', - templated => true, - ), - Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - Data::HAL::Link->new(relation => 'self', href => sprintf('/%s?page=%s&rows=%s', $c->request->path, $page, $rows)); - if(($total_count / $rows) > $page ) { - push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page + 1, $rows)); - } - if($page > 1) { - push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page - 1, $rows)); - } - - my $hal = Data::HAL->new( - embedded => [@embedded], - links => [@links], - ); - $hal->resource({ - total_count => $total_count, - }); - my $response = HTTP::Response->new(HTTP_OK, undef, - HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json); - $c->response->headers($response->headers); - $c->response->body($response->content); - return; +sub create_item { + my ($self, $c, $resource, $form, $process_extras) = @_; + unless($c->user->is_master) { + $self->error($c, HTTP_FORBIDDEN, "Cannot create admin without master permissions"); + last; } - return; -} - -sub HEAD :Allow { - my ($self, $c) = @_; - $c->forward(qw(GET)); - $c->response->body(q()); - return; -} - -sub OPTIONS :Allow { - my ($self, $c) = @_; - my $allowed_methods = $self->allowed_methods_filtered($c); - $c->response->headers(HTTP::Headers->new( - Allow => join(', ', @{ $allowed_methods }), - Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-'.$self->resource_name, - )); - $c->response->content_type('application/json'); - $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); - return; -} - -sub POST :Allow { - my ($self, $c) = @_; - - my $guard = $c->model('DB')->txn_scope_guard; - { - unless($c->user->is_master) { - $self->error($c, HTTP_FORBIDDEN, "Cannot create admin without master permissions"); - last; - } - - my $resource = $self->get_valid_post_data( - c => $c, - media_type => 'application/json', - ); - last unless $resource; - - my $form = $self->get_form($c); - my $pass = $resource->{password}; - last unless $self->validate_form( - c => $c, - resource => $resource, - form => $form, - ); - delete $resource->{password}; - if(defined $pass) { - $resource->{md5pass} = undef; - $resource->{saltedpass} = NGCP::Panel::Utils::Admin::generate_salted_hash($pass); - } - if($c->user->roles eq "admin") { - } elsif($c->user->roles eq "reseller") { - $resource->{reseller_id} = $c->user->reseller_id; - } - - my $item; - $item = $c->model('DB')->resultset('admins')->find({ - login => $resource->{login}, - }); - if($item) { - $c->log->error("admin with login '$$resource{login}' already exists"); - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Admin with this login already exists"); - last; - } - - try { - $item = $c->model('DB')->resultset('admins')->create($resource); - } catch($e) { - $c->log->error("failed to create admin: $e"); - $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create admin."); - last; - } - - $guard->commit; - - $c->response->status(HTTP_CREATED); - $c->response->header(Location => sprintf('/%s%d', $c->request->path, $item->id)); - $c->response->body(q()); + my $item; + try { + $item = $c->model('DB')->resultset('admins')->create($resource); + } catch($e) { + $c->log->error("failed to create admin: $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create admin."); + last; } - return; -} - -sub end : Private { - my ($self, $c) = @_; - - $self->log_response($c); + return $item; } 1; diff --git a/lib/NGCP/Panel/Controller/API/AdminsItem.pm b/lib/NGCP/Panel/Controller/API/AdminsItem.pm index 2cb8438e12..f260cea0c6 100644 --- a/lib/NGCP/Panel/Controller/API/AdminsItem.pm +++ b/lib/NGCP/Panel/Controller/API/AdminsItem.pm @@ -3,161 +3,66 @@ use NGCP::Panel::Utils::Generic qw(:all); use Sipwise::Base; -use NGCP::Panel::Utils::Admin; +use parent qw/NGCP::Panel::Role::EntitiesItem NGCP::Panel::Role::API::Admins/; -use boolean qw(true); -use Data::HAL qw(); -use Data::HAL::Link qw(); -use HTTP::Headers qw(); +use NGCP::Panel::Utils::Admin; use HTTP::Status qw(:constants); -require Catalyst::ActionRole::ACL; -require NGCP::Panel::Role::HTTPMethods; -require Catalyst::ActionRole::RequireSSL; - sub allowed_methods{ return [qw/GET OPTIONS HEAD DELETE/]; } -use parent qw/Catalyst::Controller NGCP::Panel::Role::API::Admins/; - -sub resource_name{ - return 'admins'; -} -sub dispatch_path{ - return '/api/admins/'; -} -sub relation{ - return 'http://purl.org/sipwise/ngcp-api/#rel-admins'; -} - sub journal_query_params { my($self,$query_params) = @_; return $self->get_journal_query_params($query_params); } -__PACKAGE__->config( - action => { - (map { $_ => { - ACLDetachTo => '/api/root/invalid_user', - AllowedRole => [qw/admin reseller/], - Args => 1, - Does => [qw(ACL RequireSSL)], - Method => $_, - Path => __PACKAGE__->dispatch_path, - } } @{ __PACKAGE__->allowed_methods }), - @{ __PACKAGE__->get_journal_action_config(__PACKAGE__->resource_name,{ - ACLDetachTo => '/api/root/invalid_user', - AllowedRole => [qw/admin reseller/], - Does => [qw(ACL RequireSSL)], - }) }, - }, -); - -sub gather_default_action_roles { - my ($self, %args) = @_; my @roles = (); - push @roles, 'NGCP::Panel::Role::HTTPMethods' if $args{attributes}->{Method}; - return @roles; -} +sub get_journal_methods{ + return [qw/handle_item_base_journal handle_journals_get handle_journalsitem_get handle_journals_options handle_journalsitem_options handle_journals_head handle_journalsitem_head/]; +} -sub auto :Private { - my ($self, $c) = @_; +__PACKAGE__->set_config(); - $self->set_body($c); - $self->log_request($c); - return 1; -} +sub delete_item { + my ($self, $c, $item) = @_; -sub GET :Allow { - my ($self, $c, $id) = @_; - { - last unless $self->valid_id($c, $id); - my $admin = $self->item_by_id($c, $id); - last unless $self->resource_exists($c, admin => $admin); - - my $hal = $self->hal_from_item($c, $admin); - - my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( - (map { # XXX Data::HAL must be able to generate links with multiple relations - s|rel="(http://purl.org/sipwise/ngcp-api/#rel-resellers)"|rel="item $1"|r =~ - s/rel=self/rel="item self"/r; - } $hal->http_headers), - ), $hal->as_json); - $c->response->headers($response->headers); - $c->response->body($response->content); - return; - } - return; -} + my $special_user_login = NGCP::Panel::Utils::Admin::get_special_admin_login(); -sub HEAD :Allow { - my ($self, $c, $id) = @_; - $c->forward(qw(GET)); - $c->response->body(q()); - return; -} + if($item->login eq $special_user_login) { + $self->error($c, HTTP_FORBIDDEN, "Cannot delete special user '$special_user_login'"); + last; + } + if($c->user->id == $item->id) { + $self->error($c, HTTP_FORBIDDEN, "Cannot delete own user"); + last; + } + if($c->user->read_only) { + $self->error($c, HTTP_FORBIDDEN, "Insufficient permissions"); + last; + } -sub OPTIONS :Allow { - my ($self, $c, $id) = @_; - my $allowed_methods = $self->allowed_methods_filtered($c); - $c->response->headers(HTTP::Headers->new( - Allow => join(', ', @{ $allowed_methods }), - Accept_Patch => 'application/json-patch+json', - )); - $c->response->content_type('application/json'); - $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); - return; -} + # reseller association is checked in item_rs of role -sub DELETE :Allow { - my ($self, $c, $id) = @_; - my $guard = $c->model('DB')->txn_scope_guard; - { - my $admin = $self->item_by_id($c, $id); - last unless $self->resource_exists($c, admin => $admin); - - $c->log->error("++++++ trying to delete admin #$id as #" . $c->user->id); - - my $special_user_login = NGCP::Panel::Utils::Admin::get_special_admin_login(); - if($admin->login eq $special_user_login) { - $self->error($c, HTTP_FORBIDDEN, "Cannot delete special user '$special_user_login'"); - last; - } - if($c->user->id == $id) { - $self->error($c, HTTP_FORBIDDEN, "Cannot delete own user"); - last; - } - if($c->user->read_only) { - $self->error($c, HTTP_FORBIDDEN, "Insufficient permissions"); - last; - } - - # reseller association is checked in item_rs of role - - last unless $self->add_delete_journal_item_hal($c,sub { - my $self = shift; - my ($c) = @_; - return $self->hal_from_item($c,$admin); }); - - $admin->delete; - - $guard->commit; - - $c->response->status(HTTP_NO_CONTENT); - $c->response->body(q()); - } + last unless $self->add_delete_journal_item_hal($c,sub { + my $self = shift; + my ($c) = @_; + return $self->hal_from_item($c, $item); }); + + $item->delete; return; } -sub get_journal_methods{ - return [qw/handle_item_base_journal handle_journals_get handle_journalsitem_get handle_journals_options handle_journalsitem_options handle_journals_head handle_journalsitem_head/]; -} +#we don't use update_item for the admins now, as we dont allo PUT and PATCH +sub update_item_model { + my ($self, $c, $item, $old_resource, $resource, $form) = @_; -sub end : Private { - my ($self, $c) = @_; - - $self->log_response($c); - return; + if($old_resource->{login} eq NGCP::Panel::Utils::Admin::get_special_admin_login()) { + my $active = $resource->{is_active}; + $resource = $old_resource; + $resource->{is_active} = $active; + } + $item->update($resource); + return $item; } 1; diff --git a/lib/NGCP/Panel/Role/API.pm b/lib/NGCP/Panel/Role/API.pm index dca78bf56a..a83aca5f1d 100644 --- a/lib/NGCP/Panel/Role/API.pm +++ b/lib/NGCP/Panel/Role/API.pm @@ -924,7 +924,8 @@ sub hal_from_item { $self->get_item_id($c, $item, undef, undef, { purpose => 'hal_links_href' }) ) ), - @$links + @$links, + $self->get_journal_relation_link($self->get_item_id($c, $item)), ], relation => 'ngcp:'.$self->resource_name, ); diff --git a/lib/NGCP/Panel/Role/API/Admins.pm b/lib/NGCP/Panel/Role/API/Admins.pm index 865c28f44b..815c65fb6e 100644 --- a/lib/NGCP/Panel/Role/API/Admins.pm +++ b/lib/NGCP/Panel/Role/API/Admins.pm @@ -1,17 +1,33 @@ package NGCP::Panel::Role::API::Admins; use NGCP::Panel::Utils::Generic qw(:all); +use NGCP::Panel::Utils::API; use Sipwise::Base; use parent 'NGCP::Panel::Role::API'; -use boolean qw(true); -use Data::HAL qw(); use Data::HAL::Link qw(); use HTTP::Status qw(:constants); + use NGCP::Panel::Utils::DateTime; use NGCP::Panel::Utils::Admin; +sub item_name{ + return 'admin'; +} + +sub resource_name{ + return 'admins'; +} + +sub dispatch_path{ + return '/api/admins/'; +} + +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-admins'; +} + sub _item_rs { my ($self, $c) = @_; @@ -22,7 +38,6 @@ sub _item_rs { }); } - if($c->user->is_master || $c->user->is_superuser) { # return all (or all of reseller) admins } else { @@ -45,98 +60,47 @@ sub get_form { return $form; } -sub hal_from_item { - my ($self, $c, $item, $form) = @_; - - my %resource = $item->get_inflated_columns; - delete $resource{md5pass}; - delete $resource{saltedpass}; - +sub hal_links { + my($self, $c, $item, $resource, $form) = @_; my $adm = $c->user->roles eq "admin"; - - my $hal = Data::HAL->new( - links => [ - Data::HAL::Link->new( - relation => 'curies', - href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', - name => 'ngcp', - templated => true, - ), - Data::HAL::Link->new(relation => 'collection', href => sprintf('%s', $self->dispatch_path)), - Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)), - $adm ? Data::HAL::Link->new(relation => 'ngcp:resellers', href => sprintf("/api/resellers/%d", $item->reseller_id)) : (), - $self->get_journal_relation_link($item->id), - ], - relation => 'ngcp:'.$self->resource_name, - ); - - $form //= $self->get_form($c); - return unless $self->validate_form( - c => $c, - form => $form, - resource => \%resource, - run => 0, - ); - - $resource{id} = int($item->id); - $hal->resource({%resource}); - return $hal; + return [ + $adm ? Data::HAL::Link->new(relation => 'ngcp:resellers', href => sprintf("/api/resellers/%d", $item->reseller_id)) : (), + ]; } -sub item_by_id { - my ($self, $c, $id) = @_; - - my $rs = $self->item_rs($c); - return $rs->find($id); -} +sub process_form_resource{ + my($self,$c, $item, $old_resource, $resource, $form, $process_extras) = @_; -#we don't use update_item for the admins now. -sub update_item { - my ($self, $c, $item, $old_resource, $resource, $form) = @_; + NGCP::Panel::Utils::API::apply_resource_reseller_id($c, $resource); - $form //= $self->get_form($c); - $resource->{contract_id} //= undef; my $pass = $resource->{password}; - return unless $self->validate_form( - c => $c, - form => $form, - resource => $resource, - ); delete $resource->{password}; if(defined $pass) { $resource->{md5pass} = undef; $resource->{saltedpass} = NGCP::Panel::Utils::Admin::generate_salted_hash($pass); } + return $resource; +} - if($old_resource->{login} eq NGCP::Panel::Utils::Admin::get_special_admin_login()) { - my $active = $resource->{is_active}; - $resource = $old_resource; - $resource->{is_active} = $active; - } - - if($c->user->roles eq "reseller" && $resource->{reseller_id} != $c->user->reseller_id) { - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'reseller_id'"); - return; - } - - if($old_resource->{reseller_id} != $resource->{reseller_id}) { - unless($c->model('DB')->resultset('resellers')->find($resource->{reseller_id})) { - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'reseller_id'"); - return; - } - } +sub check_resource{ + my($self, $c, $item, $old_resource, $resource, $form, $process_extras) = @_; + #TODO: move to config + return unless NGCP::Panel::Utils::API::check_resource_reseller_id($self, $c, $resource, $old_resource); + return 1; +} - if($old_resource->{login} ne $resource->{login}) { - my $rs = $self->item_rs($c); - if($rs->find({ login => $resource->{login} })) { - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'login', admin with this login already exists"); - return; - } +sub check_duplicate{ + my($self, $c, $item, $old_resource, $resource, $form, $process_extras) = @_; + my $schema = $c->model('DB'); + my $existing_item = $schema->resultset('admins')->find({ + login => $resource->{login}, + }); + if ($existing_item && (!$item || $item->id != $existing_item->id)) { + $c->log->error("admin with login '$$resource{login}' already exists"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Admin with this login already exists"); + last; } - - $item->update($resource); - return $item; + return 1; } 1; diff --git a/lib/NGCP/Panel/Role/API/RewriteRuleSets.pm b/lib/NGCP/Panel/Role/API/RewriteRuleSets.pm index ddf197c42a..89577a870a 100644 --- a/lib/NGCP/Panel/Role/API/RewriteRuleSets.pm +++ b/lib/NGCP/Panel/Role/API/RewriteRuleSets.pm @@ -4,6 +4,7 @@ use parent qw/NGCP::Panel::Role::API/; use Sipwise::Base; use NGCP::Panel::Utils::Generic qw(:all); +use NGCP::Panel::Utils::API; use HTTP::Status qw(:constants); sub item_name{ @@ -72,16 +73,7 @@ sub _item_rs { sub process_form_resource{ my($self,$c, $item, $old_resource, $resource, $form, $process_extras) = @_; - my $reseller_id; - if($c->user->roles eq "admin") { - try { - $reseller_id = $resource->{reseller_id} - || $c->user->contract->contact->reseller_id; - } - } elsif($c->user->roles eq "reseller") { - $reseller_id = $c->user->reseller_id; - } - $resource->{reseller_id} = $reseller_id; + NGCP::Panel::Utils::API::apply_resource_reseller_id($c, $resource); return $resource; } diff --git a/lib/NGCP/Panel/Role/EntitiesItem.pm b/lib/NGCP/Panel/Role/EntitiesItem.pm index 62f003cede..9b63de4176 100644 --- a/lib/NGCP/Panel/Role/EntitiesItem.pm +++ b/lib/NGCP/Panel/Role/EntitiesItem.pm @@ -31,6 +31,11 @@ sub set_config { %{$self->_set_config($_)}, } } @{ $self->allowed_methods } }, + @{ $self->get_journal_action_config($self->resource_name,{ + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Does => [qw(ACL RequireSSL)], + }) }, #action_roles => [qw(HTTPMethods)], log_response => 1, %{$self->_set_config()}, diff --git a/lib/NGCP/Panel/Utils/API.pm b/lib/NGCP/Panel/Utils/API.pm index a9519ae577..7dcb05ec26 100644 --- a/lib/NGCP/Panel/Utils/API.pm +++ b/lib/NGCP/Panel/Utils/API.pm @@ -3,9 +3,40 @@ package NGCP::Panel::Utils::API; use strict; use warnings; +use Sipwise::Base; use File::Find::Rule; use JSON qw(); +use HTTP::Status qw(:constants); + + +sub check_resource_reseller_id { + my($api, $c, $resource, $old_resource) = @_; + my $reseller; + if( $resource->{reseller_id} + && (( ! $old_resource ) || $old_resource->{reseller_id} != $resource->{reseller_id} )) { + $reseller = $c->model('DB')->resultset('resellers')->find($resource->{reseller_id}); + unless( $reseller ) { + $api->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'reseller_id'"); + return; + } + } + return $reseller; +} +sub apply_resource_reseller_id { + my($c, $resource) = @_; + my $reseller_id; + if($c->user->roles eq "admin") { + try { + $reseller_id = $resource->{reseller_id} + || $c->user->contract->contact->reseller_id; + } + } elsif($c->user->roles eq "reseller") { + $reseller_id = $c->user->reseller_id; + } + $resource->{reseller_id} = $reseller_id; + return $resource; +} sub get_collections { my @files = @{get_collections_files()};