diff --git a/lib/NGCP/Panel/Controller/API/AuthTokens.pm b/lib/NGCP/Panel/Controller/API/AuthTokens.pm new file mode 100644 index 0000000000..c3495b3f9f --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/AuthTokens.pm @@ -0,0 +1,74 @@ +package NGCP::Panel::Controller::API::AuthTokens; + +use Sipwise::Base; + +use Data::HAL qw(); +use Data::HAL::Link qw(); +use File::Basename; +use File::Find::Rule; +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); + + +sub allowed_methods{ + return [qw/POST OPTIONS/]; +} + +use parent qw/NGCP::Panel::Role::Entities NGCP::Panel::Role::API::AuthTokens/; + +sub api_description { + return ''; +}; + +sub query_params { + return [ + ]; +} + +sub resource_name{ + return 'authtokens'; +} + +sub dispatch_path{ + return '/api/authtokens/'; +} + +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-authtokens'; +} + +__PACKAGE__->set_config({ + allowed_roles => [qw/admin reseller ccare ccareadmin subscriber subscriberadmin/], +}); + +sub POST :Allow { + my ($self, $c) = @_; + + my $resource = $self->get_valid_post_data( + c => $c, + media_type => 'application/json', + ); + return unless $resource; + + my $form = $self->get_form($c); + return unless $self->validate_form( + c => $c, + resource => $resource, + form => $form, + ); + if($c->user->roles eq "reseller") { + $resource->{reseller_id} = $c->user->reseller_id; + } + + my $res = {}; + + $res->{token} = $self->generate_auth_token($c, $resource); + + $c->response->status(HTTP_CREATED); + $c->response->body(JSON::to_json($res)); + return; +} + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/Root.pm b/lib/NGCP/Panel/Controller/API/Root.pm index da711d6bf8..aeae0940b5 100644 --- a/lib/NGCP/Panel/Controller/API/Root.pm +++ b/lib/NGCP/Panel/Controller/API/Root.pm @@ -81,6 +81,7 @@ sub GET : Allow { "PbxDeviceProfilePreferenceDefs" => 1, "PbxFieldDevicePreferenceDefs" => 1, "MetaConfigDefs" => 1, + "AuthTokens" => 1, }; my $colls = NGCP::Panel::Utils::API::get_collections_files; diff --git a/lib/NGCP/Panel/Controller/Root.pm b/lib/NGCP/Panel/Controller/Root.pm index 3dad939324..f5dcde39d0 100644 --- a/lib/NGCP/Panel/Controller/Root.pm +++ b/lib/NGCP/Panel/Controller/Root.pm @@ -503,6 +503,7 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') { use JSON qw/encode_json decode_json/; use Crypt::JWT qw/encode_jwt/; + my $auth_token = $c->req->body_data->{token} // ''; my $user = $c->req->body_data->{username} // ''; my $pass = $c->req->body_data->{password} // ''; @@ -522,82 +523,112 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') { my $raw_key = pack('H*', $key); - unless ($user && $pass) { - $c->response->status(HTTP_UNPROCESSABLE_ENTITY); - $c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, - message => "No username or password given" })."\n"); - $c->log->error("No username or password given"); - return; - } - - if ($pass =~ /[^[:ascii:]]/) { - $c->response->status(HTTP_UNPROCESSABLE_ENTITY); - $c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, - message => "'password' contains invalid characters" })."\n"); - $c->log->error("'password' contains invalid characters"); - return; - } + my $auth_user; + if ($auth_token) { + my $redis = NGCP::Panel::Utils::Redis::get_redis_connection($c, {database => $c->config->{'Plugin::Session'}->{redis_db}}); + unless ($redis) { + $c->response->status(HTTP_INTERNAL_SERVER_ERROR); + $c->response->body(encode_json({ code => HTTP_INTERNAL_SERVER_ERROR, + message => "Internal Server Error" })."\n"); + $c->log->error("Could not connect to Redis"); + return; + } + my $type = $redis->hget("auth_token:$auth_token", "type"); + my $role = $redis->hget("auth_token:$auth_token", "role"); + my $user_id = $redis->hget("auth_token:$auth_token", "user_id"); + $redis->del("auth_token:$auth_token") if ($type eq 'onetime'); + unless (grep {$role eq $_} qw/subscriber subscriberadmin/) { + $c->response->status(HTTP_FORBIDDEN); + $c->response->body(encode_json({ code => HTTP_FORBIDDEN, + message => "Forbidden!" })."\n"); + $c->log->error("Wrong auth_token role"); + return; + } + my $authrs = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({ + 'me.id' => $user_id, + 'voip_subscriber.status' => 'active', + 'contract.status' => 'active', + }, { + join => ['contract', 'voip_subscriber'], + }); + $auth_user = $authrs->first if ($authrs->first); + } else { + unless ($user && $pass) { + $c->response->status(HTTP_UNPROCESSABLE_ENTITY); + $c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, + message => "No username or password given" })."\n"); + $c->log->error("No username or password given"); + return; + } - my ($u, $d, $t) = split(/\@/, $user, 3); - if(defined $t) { - # in case username is an email address - $u = $u . '@' . $d; - $d = $t; - } - unless(defined $d) { - $d = $c->req->uri->host; - } - my $authrs = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({ - webusername => $u, - 'voip_subscriber.status' => 'active', - 'domain.domain' => $d, - 'contract.status' => 'active', - }, { - join => ['domain', 'contract', 'voip_subscriber'], - }); + if ($pass =~ /[^[:ascii:]]/) { + $c->response->status(HTTP_UNPROCESSABLE_ENTITY); + $c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, + message => "'password' contains invalid characters" })."\n"); + $c->log->error("'password' contains invalid characters"); + return; + } - my $auth_user; - if ($authrs->first) { - my $password = $authrs->first->webpassword; - if (length $password > 40) { - my @splitted_pass = split /\$/, $password; - if (scalar @splitted_pass == 3) { - #password is bcrypted with lower cost - my ($cost, $db_b64salt, $db_b64hash) = @splitted_pass; - my $salt = de_base64($db_b64salt); - my $usr_b64hash = en_base64(bcrypt_hash({ - key_nul => 1, - cost => $cost, - salt => $salt, - }, $pass)); - if ($db_b64hash eq $usr_b64hash) { - #upgrade password to bigger cost - $salt = rand_bits(128); - my $b64salt = en_base64($salt); - my $b64hash = en_base64(bcrypt_hash({ + my ($u, $d, $t) = split(/\@/, $user, 3); + if(defined $t) { + # in case username is an email address + $u = $u . '@' . $d; + $d = $t; + } + unless(defined $d) { + $d = $c->req->uri->host; + } + my $authrs = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({ + webusername => $u, + 'voip_subscriber.status' => 'active', + 'domain.domain' => $d, + 'contract.status' => 'active', + }, { + join => ['domain', 'contract', 'voip_subscriber'], + }); + + if ($authrs->first) { + my $password = $authrs->first->webpassword; + if (length $password > 40) { + my @splitted_pass = split /\$/, $password; + if (scalar @splitted_pass == 3) { + #password is bcrypted with lower cost + my ($cost, $db_b64salt, $db_b64hash) = @splitted_pass; + my $salt = de_base64($db_b64salt); + my $usr_b64hash = en_base64(bcrypt_hash({ + key_nul => 1, + cost => $cost, + salt => $salt, + }, $pass)); + if ($db_b64hash eq $usr_b64hash) { + #upgrade password to bigger cost + $salt = rand_bits(128); + my $b64salt = en_base64($salt); + my $b64hash = en_base64(bcrypt_hash({ + key_nul => 1, + cost => NGCP::Panel::Utils::Auth::get_bcrypt_cost(), + salt => $salt, + }, $pass)); + $authrs->first->update({webpassword => $b64salt . '$' . $b64hash}); + $auth_user = $authrs->first; + } + } + elsif (scalar @splitted_pass == 2) { + #password is bcrypted with proper cost + my ($db_b64salt, $db_b64hash) = @splitted_pass; + my $salt = de_base64($db_b64salt); + my $usr_b64hash = en_base64(bcrypt_hash({ key_nul => 1, cost => NGCP::Panel::Utils::Auth::get_bcrypt_cost(), salt => $salt, }, $pass)); - $authrs->first->update({webpassword => $b64salt . '$' . $b64hash}); - $auth_user = $authrs->first; + $auth_user = $authrs->search({webpassword => $db_b64salt . '$' . $usr_b64hash})->first; } } - elsif (scalar @splitted_pass == 2) { - #password is bcrypted with proper cost - my ($db_b64salt, $db_b64hash) = @splitted_pass; - my $salt = de_base64($db_b64salt); - my $usr_b64hash = en_base64(bcrypt_hash({ - key_nul => 1, - cost => NGCP::Panel::Utils::Auth::get_bcrypt_cost(), - salt => $salt, - }, $pass)); - $auth_user = $authrs->search({webpassword => $db_b64salt . '$' . $usr_b64hash})->first; + else { + $auth_user = $authrs->search({webpassword => $pass})->first; } } - else { - $auth_user = $authrs->search({webpassword => $pass})->first; - } } my $result = {}; @@ -634,6 +665,7 @@ sub admin_login_jwt :Chained('/') :PathPart('admin_login_jwt') :Args(0) :Method( use JSON qw/encode_json decode_json/; use Crypt::JWT qw/encode_jwt/; + my $auth_token = $c->req->body_data->{token} // ''; my $user = $c->req->body_data->{username} // ''; my $pass = $c->req->body_data->{password} // ''; @@ -653,36 +685,72 @@ sub admin_login_jwt :Chained('/') :PathPart('admin_login_jwt') :Args(0) :Method( my $raw_key = pack('H*', $key); - unless ($user && $pass) { - $c->response->status(HTTP_UNPROCESSABLE_ENTITY); - $c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, - message => "No username or password given" })."\n"); - $c->log->error("No username or password given"); - return; - } + my $auth_user; + if ($auth_token) { + my $redis = NGCP::Panel::Utils::Redis::get_redis_connection($c, {database => $c->config->{'Plugin::Session'}->{redis_db}}); + unless ($redis) { + $c->response->status(HTTP_INTERNAL_SERVER_ERROR); + $c->response->body(encode_json({ code => HTTP_INTERNAL_SERVER_ERROR, + message => "Internal Server Error" })."\n"); + $c->log->error("Could not connect to Redis"); + return; + } + my $type = $redis->hget("auth_token:$auth_token", "type"); + my $role = $redis->hget("auth_token:$auth_token", "role"); + my $user_id = $redis->hget("auth_token:$auth_token", "user_id"); + $redis->del("auth_token:$auth_token") if ($type eq 'onetime'); + unless (grep {$role eq $_} qw/admin reseller ccare ccareadmin/) { + $c->response->status(HTTP_FORBIDDEN); + $c->response->body(encode_json({ code => HTTP_FORBIDDEN, + message => "Forbidden!" })."\n"); + $c->log->error("Wrong auth_token role"); + return; + } + my $authrs = $c->model('DB')->resultset('admins')->search({ + id => $user_id, + is_active => 1, + }); + $auth_user = $authrs->first if ($authrs->first); + } else { + unless ($user && $pass) { + $c->response->status(HTTP_UNPROCESSABLE_ENTITY); + $c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, + message => "No username or password given" })."\n"); + $c->log->error("No username or password given"); + return; + } - if ($pass =~ /[^[:ascii:]]/) { - $c->response->status(HTTP_UNPROCESSABLE_ENTITY); - $c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, - message => "'password' contains invalid characters" })."\n"); - $c->log->error("'password' contains invalid characters"); - return; - } + if ($pass =~ /[^[:ascii:]]/) { + $c->response->status(HTTP_UNPROCESSABLE_ENTITY); + $c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, + message => "'password' contains invalid characters" })."\n"); + $c->log->error("'password' contains invalid characters"); + return; + } - my $authrs = $c->model('DB')->resultset('admins')->search({ - login => $user, - is_active => 1, - }); + my $authrs = $c->model('DB')->resultset('admins')->search({ + login => $user, + is_active => 1, + }); - my $usr_salted_pass; - my $auth_user = $authrs->first; - my $result = {}; + my $usr_salted_pass; + $auth_user = $authrs->first; - if ($auth_user && $auth_user->id) { - $usr_salted_pass = NGCP::Panel::Utils::Auth::get_usr_salted_pass($auth_user->saltedpass, $pass); + if ($auth_user && $auth_user->id) { + $usr_salted_pass = NGCP::Panel::Utils::Auth::get_usr_salted_pass($auth_user->saltedpass, $pass); + } + + unless ($usr_salted_pass && $usr_salted_pass eq $auth_user->saltedpass) { + $c->response->status(HTTP_FORBIDDEN); + $c->response->body(encode_json({ code => HTTP_FORBIDDEN, + message => "User not found" })."\n"); + $c->log->error("User not found"); + return; + } } - if ($usr_salted_pass && $usr_salted_pass eq $auth_user->saltedpass) { + my $result = {}; + if ($auth_user) { my $jwt_data = { id => $auth_user->id, username => $auth_user->login, diff --git a/lib/NGCP/Panel/Form/AuthToken.pm b/lib/NGCP/Panel/Form/AuthToken.pm new file mode 100644 index 0000000000..77e29998ac --- /dev/null +++ b/lib/NGCP/Panel/Form/AuthToken.pm @@ -0,0 +1,29 @@ +package NGCP::Panel::Form::AuthToken; + +use HTML::FormHandler::Moose; +use NGCP::Panel::Utils::Form; +extends 'HTML::FormHandler'; + +has_field 'type' => ( + type => 'Select', + options => [ + { label => 'Onetime', value => 'onetime' }, + { label => 'Expires', value => 'expires' }, + ], + label => 'Type', + required => 1, +); + +has_field 'expires' => ( + type => '+NGCP::Panel::Field::PosInteger', + required => 1, + label => 'Expires', +); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/type expires/], +); + +1; diff --git a/lib/NGCP/Panel/Role/API/AuthTokens.pm b/lib/NGCP/Panel/Role/API/AuthTokens.pm new file mode 100644 index 0000000000..da4f0fb1a9 --- /dev/null +++ b/lib/NGCP/Panel/Role/API/AuthTokens.pm @@ -0,0 +1,35 @@ +package NGCP::Panel::Role::API::AuthTokens; + +use Sipwise::Base; +use NGCP::Panel::Utils::Redis; + +use parent 'NGCP::Panel::Role::API'; + +use Redis; +use UUID; + +sub get_form { + my ($self, $c) = @_; + return NGCP::Panel::Form::get("NGCP::Panel::Form::AuthToken", $c); +} + +sub generate_auth_token { + my ($self, $c, $resource) = @_; + + my ($uuid_bin, $uuid_string); + UUID::generate($uuid_bin); + UUID::unparse($uuid_bin, $uuid_string); + #remove '-' from the token + $uuid_string =~ s/\-//g; + my $redis = NGCP::Panel::Utils::Redis::get_redis_connection($c, {database => $c->config->{'Plugin::Session'}->{redis_db}}); + return unless $redis; + $redis->hset("auth_token:$uuid_string", 'type', $resource->{type}); + $redis->hset("auth_token:$uuid_string", 'role', $c->user->roles); + $redis->hset("auth_token:$uuid_string", 'user_id', $c->user->id); + $redis->expire("auth_token:$uuid_string", $resource->{expires}); + + return $uuid_string; +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Utils/Redis.pm b/lib/NGCP/Panel/Utils/Redis.pm new file mode 100644 index 0000000000..9ec29323d6 --- /dev/null +++ b/lib/NGCP/Panel/Utils/Redis.pm @@ -0,0 +1,23 @@ +package NGCP::Panel::Utils::Redis; + +use warnings; +use strict; + +use Redis; + +sub get_redis_connection { + my ($c, $params) = @_; + my $redis = Redis->new( + server => $c->config->{redis}->{central_url}, + reconnect => 10, every => 500000, # 500ms + cnx_timeout => 3, + ); + unless ($redis) { + $c->log->error("Failed to connect to central redis url " . $c->config->{redis}->{central_url}); + return; + } + $redis->select($params->{database}); + return $redis; +} + +1; diff --git a/t/api-rest/api-root.t b/t/api-rest/api-root.t index f4e6ed628e..ce3329ab0e 100644 --- a/t/api-rest/api-root.t +++ b/t/api-rest/api-root.t @@ -46,6 +46,7 @@ $ua = Test::Collection->new()->ua(); admincerts => 1, admins => 1, applyrewrites => 1, + authtokens => 1, autoattendants => 1, balanceintervals => 1, bannedips => 1,