Tenant API
Project: http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/commit/da494df7 Tree: http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/tree/da494df7 Diff: http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/diff/da494df7 Branch: refs/heads/master Commit: da494df7d1891d95703f55eb15272dbfa92bf260 Parents: d936a34 Author: nir-sopher <nirsop...@gmail.com> Authored: Wed Mar 15 15:50:58 2017 +0200 Committer: Jeremy Mitchell <mitchell...@gmail.com> Committed: Sun Mar 19 19:08:28 2017 -0600 ---------------------------------------------------------------------- traffic_ops/app/lib/API/Tenant.pm | 247 +++++++++++++++++++++++++++ traffic_ops/app/lib/TrafficOpsRoutes.pm | 7 + traffic_ops/app/t/api/1.2/tenant.t | 155 +++++++++++++++++ 3 files changed, 409 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/blob/da494df7/traffic_ops/app/lib/API/Tenant.pm ---------------------------------------------------------------------- diff --git a/traffic_ops/app/lib/API/Tenant.pm b/traffic_ops/app/lib/API/Tenant.pm new file mode 100644 index 0000000..309b99c --- /dev/null +++ b/traffic_ops/app/lib/API/Tenant.pm @@ -0,0 +1,247 @@ +package API::Tenant; +# +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +# + +use UI::Utils; + +use Mojo::Base 'Mojolicious::Controller'; +use Data::Dumper; +use JSON; +use MojoPlugins::Response; + +my $finfo = __FILE__ . ":"; + +sub getTenantName { + my $self = shift; + my $tenant_id = shift; + return defined($tenant_id) ? $self->db->resultset('Tenant')->search( { id => $tenant_id } )->get_column('name')->single() : "n/a"; +} + +sub isRootTenant { + my $self = shift; + my $tenant_id = shift; + return !defined($self->db->resultset('Tenant')->search( { id => $tenant_id } )->get_column('parent_id')->single()); +} + +sub index { + my $self = shift; + + my @data; + my $orderby = $self->param('orderby') || "name"; + my $rs_data = $self->db->resultset("Tenant")->search( undef, {order_by => 'me.' . $orderby } ); + while ( my $row = $rs_data->next ) { + push( + @data, { + "id" => $row->id, + "name" => $row->name, + "active" => \$row->active, + "parentId" => $row->parent_id, + } + ); + } + $self->success( \@data ); +} + + +sub show { + my $self = shift; + my $id = $self->param('id'); + + my $rs_data = $self->db->resultset("Tenant")->search( { 'me.id' => $id }); + my @data = (); + while ( my $row = $rs_data->next ) { + push( + @data, { + "id" => $row->id, + "name" => $row->name, + "active" => \$row->active, + "parentId" => $row->parent_id, + } + ); + } + $self->success( \@data ); +} + +sub update { + my $self = shift; + my $id = $self->param('id'); + my $params = $self->req->json; + + if ( !&is_oper($self) ) { + return $self->forbidden(); + } + + my $tenant = $self->db->resultset('Tenant')->find( { id => $id } ); + if ( !defined($tenant) ) { + return $self->not_found(); + } + + if ( !defined($params) ) { + return $self->alert("Parameters must be in JSON format."); + } + + if ( !defined( $params->{name} ) ) { + return $self->alert("Tenant name is required."); + } + + if ( $params->{name} ne $self->getTenantName($id) ) { + my $name = $params->{name}; + my $existing = $self->db->resultset('Tenant')->search( { name => $name } )->get_column('name')->single(); + if ($existing) { + return $self->alert("A tenant with name \"$name\" already exists."); + } + } + + if ( !defined( $params->{parentId}) && !$self->isRootTenant($id) ) { + return $self->alert("Parent Id is required."); + } + + my $is_active = $params->{active}; + + if ( !$params->{active} && $self->isRootTenant($id)) { + return $self->alert("Root user cannot be in-active."); + } + + + if ( !defined($params->{parentId}) && !isRootTenant($id) ) { + return $self->alert("Only the \"root\" tenant can have no parent."); + } + + my $values = { + name => $params->{name}, + active => $params->{active}, + parent_id => $params->{parentId} + }; + + my $rs = $tenant->update($values); + if ($rs) { + my $response; + $response->{id} = $rs->id; + $response->{name} = $rs->name; + $response->{active} = \$rs->active; + $response->{parentId} = $rs->parent_id; + $response->{lastUpdated} = $rs->last_updated; + &log( $self, "Updated Tenant name '" . $rs->name . "' for id: " . $rs->id, "APICHANGE" ); + return $self->success( $response, "Tenant update was successful." ); + } + else { + return $self->alert("Tenant update failed."); + } + +} + + +sub create { + my $self = shift; + my $params = $self->req->json; + + if ( !&is_oper($self) ) { + return $self->forbidden(); + } + + my $name = $params->{name}; + if ( !defined($name) ) { + return $self->alert("Tenant name is required."); + } + + my $parent_id = $params->{parentId}; + if ( !defined($parent_id) ) { + return $self->alert("Parent Id is required."); + } + + my $existing = $self->db->resultset('Tenant')->search( { name => $name } )->get_column('name')->single(); + if ($existing) { + return $self->alert("A tenant with name \"$name\" already exists."); + } + + my $is_active = exists($params->{active})? $params->{active} : 0; #optional, if not set use default + + if ( !$is_active && !defined($parent_id)) { + return $self->alert("Root user cannot be in-active."); + } + + my $values = { + name => $params->{name} , + active => $is_active, + parent_id => $params->{parentId} + }; + + my $insert = $self->db->resultset('Tenant')->create($values); + my $rs = $insert->insert(); + if ($rs) { + my $response; + $response->{id} = $rs->id; + $response->{name} = $rs->name; + $response->{active} = \$rs->active; + $response->{parentId} = $rs->parent_id; + $response->{lastUpdated} = $rs->last_updated; + + &log( $self, "Created Tenant name '" . $rs->name . "' for id: " . $rs->id, "APICHANGE" ); + + return $self->success( $response, "Tenant create was successful." ); + } + else { + return $self->alert("Tenant create failed."); + } + +} + + +sub delete { + my $self = shift; + my $id = $self->param('id'); + + if ( !&is_oper($self) ) { + return $self->forbidden(); + } + + my $tenant = $self->db->resultset('Tenant')->find( { id => $id } ); + if ( !defined($tenant) ) { + return $self->not_found(); + } + my $name = $self->db->resultset('Tenant')->search( { id => $id } )->get_column('name')->single(); + + my $existing_child = $self->db->resultset('Tenant')->search( { parent_id => $id } )->get_column('name')->first(); + if ($existing_child) { + return $self->alert("Tenant '$name' has children tenant(s): e.g '$existing_child'. Please update these tenants and retry."); + } + + #The order of the below tests is intentional - allowing UT to cover all cases - TODO(nirs) remove this comment when a full "tenancy" UT is added, including permissions and such (no use in putting effort into it yet) + #TODO(nirs) - add back when making available: my $existing_ds = $self->db->resultset('Deliveryservice')->search( { tenant_id => $id })->get_column('xml_id')->first(); + #TODO(nirs) - add back when making available: if ($existing_ds) { + #TODO(nirs) - add back when making available: return $self->alert("Tenant '$name' is assign with delivery-services(s): e.g. '$existing_ds'. Please update/delete these delivery-services and retry."); + #TODO(nirs) - add back when making available: } + + #TODO(nirs) - add back when making available: my $existing_cdn = $self->db->resultset('Cdn')->search( { tenant_id => $id })->get_column('name')->first(); + #TODO(nirs) - add back when making available: if ($existing_cdn) { + #TODO(nirs) - add back when making available: return $self->alert("Tenant '$name' is assign with CDNs(s): e.g. '$existing_cdn'. Please update/delete these CDNs and retry."); + #TODO(nirs) - add back when making available: } + + #TODO(nirs) - add back when making available: my $existing_user = $self->db->resultset('TmUser')->search( { tenant_id => $id })->get_column('username')->first(); + #TODO(nirs) - add back when making available: if ($existing_user) { + #TODO(nirs) - add back when making available: return $self->alert("Tenant '$name' is assign with user(s): e.g. '$existing_user'. Please update these users and retry."); + #TODO(nirs) - add back when making available: } + + my $rs = $tenant->delete(); + if ($rs) { + return $self->success_message("Tenant deleted."); + } else { + return $self->alert( "Tenant delete failed." ); + } +} + + http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/blob/da494df7/traffic_ops/app/lib/TrafficOpsRoutes.pm ---------------------------------------------------------------------- diff --git a/traffic_ops/app/lib/TrafficOpsRoutes.pm b/traffic_ops/app/lib/TrafficOpsRoutes.pm index 2b960eb..083636a 100644 --- a/traffic_ops/app/lib/TrafficOpsRoutes.pm +++ b/traffic_ops/app/lib/TrafficOpsRoutes.pm @@ -557,6 +557,13 @@ sub api_routes { # Supports ?orderby=key $r->get("/api/$version/deliveryserviceserver")->over( authenticated => 1 )->to( 'DeliveryServiceServer#index', namespace => $namespace ); + # -- TENANTS + $r->get("/api/$version/tenants")->over( authenticated => 1 )->to( 'Tenant#index', namespace => $namespace ); + $r->get( "/api/$version/tenants/:id" => [ id => qr/\d+/ ] )->over( authenticated => 1 )->to( 'Tenant#show', namespace => $namespace ); + $r->put("/api/$version/tenants/:id")->over( authenticated => 1 )->to( 'Tenant#update', namespace => $namespace ); + $r->post("/api/$version/tenants")->over( authenticated => 1 )->to( 'Tenant#create', namespace => $namespace ); + $r->delete("/api/$version/tenants/:id")->over( authenticated => 1 )->to( 'Tenant#delete', namespace => $namespace ); + # -- DIVISIONS $r->get("/api/$version/divisions")->over( authenticated => 1 )->to( 'Division#index', namespace => $namespace ); $r->get( "/api/$version/divisions/:id" => [ id => qr/\d+/ ] )->over( authenticated => 1 )->to( 'Division#show', namespace => $namespace ); http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/blob/da494df7/traffic_ops/app/t/api/1.2/tenant.t ---------------------------------------------------------------------- diff --git a/traffic_ops/app/t/api/1.2/tenant.t b/traffic_ops/app/t/api/1.2/tenant.t new file mode 100644 index 0000000..21b78b4 --- /dev/null +++ b/traffic_ops/app/t/api/1.2/tenant.t @@ -0,0 +1,155 @@ +package main; +# +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +use Mojo::Base -strict; +use Test::More; +use Test::Mojo; +use DBI; +use strict; +use warnings; +no warnings 'once'; +use warnings 'all'; +use Test::TestHelper; + +#no_transactions=>1 ==> keep fixtures after every execution, beware of duplicate data! +#no_transactions=>0 ==> delete fixtures after every execution + +BEGIN { $ENV{MOJO_MODE} = "test" } + +my $schema = Schema->connect_to_database; +my $dbh = Schema->database_handle; +my $t = Test::Mojo->new('TrafficOps'); + +Test::TestHelper->unload_core_data($schema); +Test::TestHelper->load_core_data($schema); + +ok $t->post_ok( '/login', => form => { u => Test::TestHelper::ADMIN_USER, p => Test::TestHelper::ADMIN_USER_PASSWORD } )->status_is(302) + ->or( sub { diag $t->tx->res->content->asset->{content}; } ), 'Should login?'; + +#verifying the basic cfg +$t->get_ok("/api/1.2/tenants")->status_is(200)->json_is( "/response/0/name", "root" )->or( sub { diag $t->tx->res->content->asset->{content}; } );; + +my $root_tenant_id = &get_tenant_id('root'); + +#setting with no "active" field which is optional +ok $t->post_ok('/api/1.2/tenants' => {Accept => 'application/json'} => json => { + "name" => "tenantA", "parentId" => $root_tenant_id })->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content}; } ) + ->json_is( "/response/name" => "tenantA" ) + ->json_is( "/response/active" => 0) + ->json_is( "/response/parentId" => $root_tenant_id) + , 'Does the tenant details return?'; + +#same name - would not accept +ok $t->post_ok('/api/1.2/tenants' => {Accept => 'application/json'} => json => { + "name" => "tenantA", "active" => 1, "parentId" => $root_tenant_id })->status_is(400); + +#no name - would not accept +ok $t->post_ok('/api/1.2/tenants' => {Accept => 'application/json'} => json => { + "parentId" => $root_tenant_id })->status_is(400); + +#no parent - would not accept +ok $t->post_ok('/api/1.2/tenants' => {Accept => 'application/json'} => json => { + "name" => "tenantB" })->status_is(400); + +my $tenantA_id = &get_tenant_id('tenantA'); +#rename, and move to active +ok $t->put_ok('/api/1.2/tenants/' . $tenantA_id => {Accept => 'application/json'} => json => { + "name" => "tenantA2", "active" => 1, "parentId" => $root_tenant_id + }) + ->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content}; } ) + ->json_is( "/response/name" => "tenantA2" ) + ->json_is( "/response/id" => $tenantA_id ) + ->json_is( "/response/active" => 1 ) + ->json_is( "/response/parentId" => $root_tenant_id ) + ->json_is( "/alerts/0/level" => "success" ) + , 'Does the tenantA2 details return?'; + +#change "active" +ok $t->put_ok('/api/1.2/tenants/' . $tenantA_id => {Accept => 'application/json'} => json => { + "name" => "tenantA2", "active" => 0, "parentId" => $root_tenant_id + }) + ->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content}; } ) + ->json_is( "/response/name" => "tenantA2" ) + ->json_is( "/response/id" => $tenantA_id ) + ->json_is( "/response/active" => 0 ) + ->json_is( "/response/parentId" => $root_tenant_id ) + ->json_is( "/alerts/0/level" => "success" ) + , 'Did we moved to non active?'; + +#change "active" back +ok $t->put_ok('/api/1.2/tenants/' . $tenantA_id => {Accept => 'application/json'} => json => { + "name" => "tenantA2", "active" => 1, "parentId" => $root_tenant_id + }) + ->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content}; } ) + ->json_is( "/response/name" => "tenantA2" ) + ->json_is( "/response/id" => $tenantA_id ) + ->json_is( "/response/active" => 1 ) + ->json_is( "/response/parentId" => $root_tenant_id ) + ->json_is( "/alerts/0/level" => "success" ) + , 'Did we moved back to active?'; + +#cannot change tenant parent to undef +ok $t->put_ok('/api/1.2/tenants/' . $tenantA_id => {Accept => 'application/json'} => json => { + "name" => "tenantC", + })->status_is(400); + +#cannot change root-tenant to inactive +ok $t->put_ok('/api/1.2/tenants/' . $root_tenant_id => {Accept => 'application/json'} => json => { + "name" => "root", "active" => 0, "parentId" => undef + })->status_is(400); + +#adding a child tenant +ok $t->post_ok('/api/1.2/tenants' => {Accept => 'application/json'} => json => { + "name" => "tenantD", "active" => 1, "parentId" => $tenantA_id })->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content}; } ) + ->json_is( "/response/name" => "tenantD" ) + ->json_is( "/response/active" => 1 ) + ->json_is( "/response/parentId" => $tenantA_id) + , 'Does the tenant details return?'; + +#adding a child inactive tenant +ok $t->post_ok('/api/1.2/tenants' => {Accept => 'application/json'} => json => { + "name" => "tenantE", "active" => 0, "parentId" => $tenantA_id })->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content}; } ) + ->json_is( "/response/name" => "tenantE" ) + ->json_is( "/response/active" => 0 ) + ->json_is( "/response/parentId" => $tenantA_id) + , 'Does the tenant details return?'; + +#cannot delete a tenant that have children +ok $t->delete_ok('/api/1.2/tenants/' . $tenantA_id)->status_is(400) + ->json_is( "/alerts/0/text" => "Tenant 'tenantA2' has children tenant(s): e.g 'tenantD'. Please update these tenants and retry." ) + ->or( sub { diag $t->tx->res->content->asset->{content}; } ); + +my $tenantD_id = &get_tenant_id('tenantD'); +my $tenantE_id = &get_tenant_id('tenantE'); + +ok $t->delete_ok('/api/1.2/tenants/' . $tenantE_id)->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content}; } ); +ok $t->delete_ok('/api/1.2/tenants/' . $tenantD_id)->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content}; } ); +ok $t->delete_ok('/api/1.2/tenants/' . $tenantA_id)->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content}; } ); + +ok $t->get_ok('/logout')->status_is(302)->or( sub { diag $t->tx->res->content->asset->{content}; } ); +$dbh->disconnect(); +done_testing(); + +sub get_tenant_id { + my $name = shift; + my $q = "select id from tenant where name = \'$name\'"; + my $get_svr = $dbh->prepare($q); + $get_svr->execute(); + my $p = $get_svr->fetchall_arrayref( {} ); + $get_svr->finish(); + my $id = $p->[0]->{id}; + return $id; +} +