diff --git a/lib/NGCP/Panel/Controller/API/PasswordReset.pm b/lib/NGCP/Panel/Controller/API/PasswordReset.pm new file mode 100644 index 0000000000..d82140b63a --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PasswordReset.pm @@ -0,0 +1,116 @@ +package NGCP::Panel::Controller::API::PasswordReset; +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 HTTP::Status qw(:constants); + + +sub allowed_methods{ + return [qw/POST OPTIONS/]; +} + +use parent qw/NGCP::Panel::Role::Entities NGCP::Panel::Role::API::PasswordReset/; + +sub api_description { + return 'Request a password reset using administrator email or subscriber SIP URI (username@domain).'; +} + +sub query_params { + return [ + ]; +} + +sub resource_name{ + return 'passwordreset'; +} + +sub dispatch_path{ + return '/api/passwordreset/'; +} + +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-passwordreset'; +} + +__PACKAGE__->set_config({ + action => { + map { $_ => { + Args => 0, + Does => [qw(CheckTrailingSlash RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods }, + }, +}); + +sub POST :Allow { + my ($self, $c) = @_; + + my $res; + + my $guard = $c->model('DB')->txn_scope_guard; + { + my $resource = $self->get_valid_post_data( + c => $c, + media_type => 'application/json', + ); + last unless $resource; + + my $form = $self->get_form($c); + last unless $self->validate_form( + c => $c, + resource => $resource, + form => $form, + ); + + if ($resource->{type} eq 'administrator') { + my $admin = $c->model('DB')->resultset('admins')->search({ + 'me.login' => $resource->{username} + })->first; + if($admin && $admin->email && $admin->can_reset_password) { + NGCP::Panel::Utils::Auth::initiate_password_reset($c, $admin); + } + } + elsif($resource->{type} eq 'subscriber') { + my ($user, $domain) = ($resource->{username}, $resource->{domain}); + my $subscriber = $c->model('DB')->resultset('voip_subscribers')->find({ + username => $user, + 'domain.domain' => $domain, + },{ + join => 'domain', + }); + + if($subscriber) { + # don't clear web password, a user might just have guessed it and + # could then block the legit user out + my ($uuid_bin, $uuid_string); + UUID::generate($uuid_bin); + UUID::unparse($uuid_bin, $uuid_string); + $subscriber->password_resets->delete; # clear any old entries of this subscriber + $subscriber->password_resets->create({ + uuid => $uuid_string, + timestamp => NGCP::Panel::Utils::DateTime::current_local->epoch + 300, #expire in 5 minutes + }); + my $url = $c->uri_for_action('/subscriber/recover_webpassword')->as_string . '?uuid=' . $uuid_string; + NGCP::Panel::Utils::Email::password_reset($c, $subscriber, $url); + } + } + + $guard->commit; + + $res = { success => 1, message => 'Please check your email for password reset instructions.' }; + + $c->response->status(HTTP_OK); + $c->response->body(JSON::to_json($res)); + } + return; +} + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/Login.pm b/lib/NGCP/Panel/Controller/Login.pm index 5147a49ef2..eba808ff46 100644 --- a/lib/NGCP/Panel/Controller/Login.pm +++ b/lib/NGCP/Panel/Controller/Login.pm @@ -112,37 +112,14 @@ sub reset_password :Chained('/') :PathPart('resetpassword') :Args(0) { ); } elsif ($admin->can_reset_password) { - # don't clear password, a user might just have guessed it and - # could then block the legit user out - my ($uuid_bin, $uuid_string); - UUID::generate($uuid_bin); - UUID::unparse($uuid_bin, $uuid_string); - 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($c->config->{'Plugin::Session'}->{redis_db}); - $username = $admin->login; - if ($redis->exists("password_reset:admin::$username")) { - NGCP::Panel::Utils::Message::info( + my $result = NGCP::Panel::Utils::Auth::initiate_password_reset($c, $admin); + if (!$result->{success}) { + NGCP::Panel::Utils::Message::error( c => $c, - desc => $c->loc('A password reset attempt has been made already recently, please check your email'), + desc => $c->loc($result->{error}), ); } else { - $redis->hset("password_reset:admin::$username", 'token', $uuid_string); - $redis->expire("password_reset:admin::$username", 300); - $redis->hset("password_reset:admin::$uuid_string", 'user', $username); - $redis->hset("password_reset:admin::$uuid_string", 'ip', $c->req->address); - $redis->expire("password_reset:admin::$uuid_string", 300); - my $url = $c->uri_for_action('/login/recover_password')->as_string . '?token=' . $uuid_string; - NGCP::Panel::Utils::Email::admin_password_reset($c, $admin, $url); - NGCP::Panel::Utils::Message::info( c => $c, desc => $c->loc('Successfully reset password, please check your email'), diff --git a/lib/NGCP/Panel/Controller/Root.pm b/lib/NGCP/Panel/Controller/Root.pm index 1dddbbf91e..d30b147afd 100644 --- a/lib/NGCP/Panel/Controller/Root.pm +++ b/lib/NGCP/Panel/Controller/Root.pm @@ -125,7 +125,7 @@ sub auto :Private { or $c->req->uri->path =~ m|^/recoverwebpassword/?$| or $c->req->uri->path =~ m|^/resetwebpassword/?$| or $c->req->uri->path =~ m|^/resetpassword/?$| - or $c->req->uri->path =~ m|^/ajax_logout/?$| + or $c->req->uri->path =~ m|^/api/passwordreset/?$| or $c->req->uri->path =~ m|^/internalsms/receive/?$| or $c->req->uri->path =~ m|^/soap/intercept(\.wsdl)?/?$|i ) { diff --git a/lib/NGCP/Panel/Form/PasswordResetAPI.pm b/lib/NGCP/Panel/Form/PasswordResetAPI.pm new file mode 100644 index 0000000000..b80dcdbda8 --- /dev/null +++ b/lib/NGCP/Panel/Form/PasswordResetAPI.pm @@ -0,0 +1,68 @@ +package NGCP::Panel::Form::PasswordResetAPI; + +use HTML::FormHandler::Moose; +use Email::Valid; +extends 'HTML::FormHandler'; + +has_field 'username' => ( + type => 'Text', + required => 1, + label => 'Username', +); + +has_field 'domain' => ( + type => 'Text', + required => 0, + label => 'Domain', +); + +has_field 'type' => ( + type => 'Select', + options => [ + { value => 'administrator', label => 'Administrator' }, + { value => 'subscriber', label => 'Subscriber' }, + ], + required => 1, + label => 'Type', +); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/username/], +); + +sub validate { + my ($self) = @_; + my $c = $self->ctx; + return unless $c; + + my $resource = Storable::dclone($self->values); + if ($resource->{type} eq 'administrator') { + my $address = $resource->{username}.'@ngcp.local'; + unless (Email::Valid->address($address)) { + my $err = "'username' is not valid."; + $c->log->error($err); + $self->field('username')->add_error($err); + } + } + elsif ($resource->{type} eq 'subscriber') { + my $err; + if (!$resource->{domain}) { + $err = "'domain' field is required when requesting a password reset for a subscriber"; + } + else { + my $address = $resource->{username}.'@'.$resource->{domain}; + unless (Email::Valid->address($address)) { + $err = "username and domain combination is not valid."; + } + } + if ($err) { + $c->log->error($err); + $self->field('username')->add_error($err); + } + } +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/PasswordReset.pm b/lib/NGCP/Panel/Role/API/PasswordReset.pm new file mode 100644 index 0000000000..a38118bbe9 --- /dev/null +++ b/lib/NGCP/Panel/Role/API/PasswordReset.pm @@ -0,0 +1,24 @@ +package NGCP::Panel::Role::API::PasswordReset; +use NGCP::Panel::Utils::Generic qw(:all); + +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); + + +sub _item_rs { +} + +sub get_form { + my ($self, $c) = @_; + return NGCP::Panel::Form::get("NGCP::Panel::Form::PasswordResetAPI", $c); +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Utils/Auth.pm b/lib/NGCP/Panel/Utils/Auth.pm index 8dfcdb207f..0c844ccbc1 100644 --- a/lib/NGCP/Panel/Utils/Auth.pm +++ b/lib/NGCP/Panel/Utils/Auth.pm @@ -5,6 +5,8 @@ use Crypt::Eksblowfish::Bcrypt qw/bcrypt_hash en_base64 de_base64/; use Data::Entropy::Algorithms qw/rand_bits/; use IO::Compress::Zip qw/zip/; use IPC::System::Simple qw/capturex/; +use Redis; +use UUID; sub get_special_admin_login { @@ -385,4 +387,36 @@ sub cmd { return $output; } +sub initiate_password_reset { + my ($c, $admin) = @_; + # don't clear password, a user might just have guessed it and + # could then block the legit user out + my ($uuid_bin, $uuid_string); + UUID::generate($uuid_bin); + UUID::unparse($uuid_bin, $uuid_string); + my $redis = Redis->new( + server => $c->config->{redis}->{central_url}, + reconnect => 10, every => 500000, # 500ms + cnx_timeout => 3, + ); + unless ($redis) { + return {success => 0, error => "Failed to connect to central redis url " . $c->config->{redis}->{central_url}}; + } + $redis->select($c->config->{'Plugin::Session'}->{redis_db}); + my $username = $admin->login; + if ($redis->exists("password_reset:admin::$username")) { + return {success => 0, error => 'A password reset attempt has been made already recently, please check your email.'}; + } + else { + $redis->hset("password_reset:admin::$username", 'token', $uuid_string); + $redis->expire("password_reset:admin::$username", 300); + $redis->hset("password_reset:admin::$uuid_string", 'user', $username); + $redis->hset("password_reset:admin::$uuid_string", 'ip', $c->req->address); + $redis->expire("password_reset:admin::$uuid_string", 300); + my $url = $c->uri_for_action('/login/recover_password')->as_string . '?token=' . $uuid_string; + NGCP::Panel::Utils::Email::admin_password_reset($c, $admin, $url); + } + return {success => 1}; +} + 1; diff --git a/t/api-rest/api-root.t b/t/api-rest/api-root.t index 600ec1e1d3..90489a6761 100644 --- a/t/api-rest/api-root.t +++ b/t/api-rest/api-root.t @@ -109,6 +109,7 @@ $ua = Test::Collection->new()->ua(); ncospatterns => 1, numbers => 1, partycallcontrols => 1, + passwordreset => 1, pbxdeviceconfigfiles => 1, pbxdeviceconfigs => 1, pbxdevicefirmwarebinaries => 1, @@ -171,7 +172,7 @@ $ua = Test::Collection->new()->ua(); vouchers => 1, }; foreach my $link(@links) { - my $rex = qr!^; rel="collection http://purl\.org/sipwise/ngcp-api/#rel-([a-z]+s|topupcash|managersecretary)"$!; + my $rex = qr!^; rel="collection http://purl\.org/sipwise/ngcp-api/#rel-([a-z]+s|topupcash|managersecretary|passwordreset)"$!; like($link, $rex, "check for valid link syntax"); my ($relname) = ($link =~ $rex); ok(exists $rels->{$relname}, "check for '$relname' collection in Link");