From bbd44f16faac53a912e6d3854ad095b7dafe03fe Mon Sep 17 00:00:00 2001 From: Kirill Solomko Date: Wed, 4 Sep 2024 19:15:05 +0200 Subject: [PATCH] MT#60858 add /api/passwordchange endpoint * the new endpoint accepts new_password fields and enables for authenticated user a mechanism to quickly change their password * the global password validation rules are enabled * returns 204 No Content * if user's password is expired, the endpoint is the only accessible for the user to change the password and unlock other endpoints. * a few fixes in validate_password() to correctly fetch provisioning subscriber for password change scenarios and webpassword field Change-Id: I906fcfe5c780b850d322b46b445b54c054767673 --- .../Panel/Controller/API/PasswordChange.pm | 95 +++++++++++++++++++ lib/NGCP/Panel/Form/PasswordChangeAPI.pm | 29 ++++++ lib/NGCP/Panel/Role/API.pm | 6 +- lib/NGCP/Panel/Role/API/Admins.pm | 7 -- lib/NGCP/Panel/Role/API/PasswordChange.pm | 24 +++++ lib/NGCP/Panel/Role/API/Preferences.pm | 2 - lib/NGCP/Panel/Role/API/Subscribers.pm | 7 -- lib/NGCP/Panel/Utils/Form.pm | 19 +++- 8 files changed, 166 insertions(+), 23 deletions(-) create mode 100644 lib/NGCP/Panel/Controller/API/PasswordChange.pm create mode 100644 lib/NGCP/Panel/Form/PasswordChangeAPI.pm create mode 100644 lib/NGCP/Panel/Role/API/PasswordChange.pm diff --git a/lib/NGCP/Panel/Controller/API/PasswordChange.pm b/lib/NGCP/Panel/Controller/API/PasswordChange.pm new file mode 100644 index 0000000000..0c18a81423 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PasswordChange.pm @@ -0,0 +1,95 @@ +package NGCP::Panel::Controller::API::PasswordChange; +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); + +use NGCP::Panel::Utils::Auth; +use NGCP::Panel::Utils::Admin; +use NGCP::Panel::Utils::Subscriber; + +sub allowed_methods{ + return [qw/POST OPTIONS/]; +} + +use parent qw/NGCP::Panel::Role::Entities NGCP::Panel::Role::API::PasswordChange/; + +sub api_description { + return 'Change password of the authenticated user.'; +} + +sub query_params { + return [ + ]; +} + +sub resource_name{ + return 'passwordchange'; +} + +sub dispatch_path{ + return '/api/passwordchange/'; +} + +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-passwordchange'; +} + +__PACKAGE__->set_config({ + action => { + map { $_ => { + Args => 0, + Does => [qw(CheckTrailingSlash RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods }, + }, +}); + +sub return_representation_post {} + +sub create_item { + my ($self, $c, $resource, $form, $process_extras) = @_; + + my $item = $c->user; + + try { + require Data::Dumper; + print Data::Dumper->Dumpxs([$resource]); + my $new_password = $resource->{new_password} // ''; + my $ngcp_realm = $c->request->env->{NGCP_REALM} // 'admin'; + if ($ngcp_realm eq 'admin') { + $item->update({ + saltedpass => NGCP::Panel::Utils::Auth::generate_salted_hash($new_password), + }); + NGCP::Panel::Utils::Admin::insert_password_journal( + $c, $item, $new_password + ); + } elsif ($ngcp_realm eq 'subscriber') { + $item->update({ + webpassword => $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS + ? NGCP::Panel::Utils::Auth::generate_salted_hash($new_password) + : $new_password, + }); + NGCP::Panel::Utils::Subscriber::insert_webpassword_journal( + $c, $item, $new_password + ); + } + } catch ($e) { + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to change password.", $e); + } + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + + return $item; +} + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/PasswordChangeAPI.pm b/lib/NGCP/Panel/Form/PasswordChangeAPI.pm new file mode 100644 index 0000000000..74aac93bb1 --- /dev/null +++ b/lib/NGCP/Panel/Form/PasswordChangeAPI.pm @@ -0,0 +1,29 @@ +package NGCP::Panel::Form::PasswordChangeAPI; + +use HTML::FormHandler::Moose; +use Email::Valid; +use NGCP::Panel::Utils::Form; +extends 'HTML::FormHandler'; + +has_field 'new_password' => ( + type => 'Password', + required => 1, + label => 'Password', +); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/new_password/], +); + +sub validate_new_password { + my ($self, $field) = @_; + my $c = $self->form->ctx; + return unless $c; + + NGCP::Panel::Utils::Form::validate_password(c => $c, field => $field, utf8 => 0, password_change => 1); +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API.pm b/lib/NGCP/Panel/Role/API.pm index 1f9ed3bb7a..865781105f 100644 --- a/lib/NGCP/Panel/Role/API.pm +++ b/lib/NGCP/Panel/Role/API.pm @@ -263,7 +263,7 @@ sub validate_form { my $in = (defined $_->input && ref $_->input eq 'HASH' && exists $_->input->{id}) ? $_->input->{id} : ($_->input // ''); $in //= ''; my $field_name = ($_->parent->$_isa('HTML::FormHandler::Field') ? $_->parent->name . '_' : '') . $_->name; - my $secure_input = $field_name =~ /^(web)?password$/ ? '*****' : $in; + my $secure_input = $field_name =~ /^(web|new_)?password$/ ? '*****' : $in; sprintf 'field=\'%s\', input=\'%s\', errors=\'%s\'', $field_name, $secure_input, #for now, we dont change the error response text, even if causes sensitive data in the logs. @@ -1785,8 +1785,8 @@ sub validate_request { } if (! NGCP::Panel::Utils::Auth::check_max_age($c)) { - if ($c->req->method =~ /^(PUT|PATCH)$/ && $c->req->path =~ /^api\/(admins|subscribers)\//) { - $c->stash->{validate_password_change} = 1; + if ($c->req->method eq 'POST' && $c->req->path =~ /^api\/passwordchange\//) { + $c->stash->{password_change_request} = 1; } else { $self->error($c, HTTP_FORBIDDEN, "Password expired"); return; diff --git a/lib/NGCP/Panel/Role/API/Admins.pm b/lib/NGCP/Panel/Role/API/Admins.pm index 7a4bc3549c..0b15fb74d8 100644 --- a/lib/NGCP/Panel/Role/API/Admins.pm +++ b/lib/NGCP/Panel/Role/API/Admins.pm @@ -155,13 +155,6 @@ sub update_item { resource => $resource, ); - if ($c->stash->{validate_password_change}) { - if (!$resource->{password} || $resource->{password} eq $old_resource->{saltedpass}) { - $self->error($c, HTTP_FORBIDDEN, "Password expired"); - return; - } - } - if ($item->id == $c->user->id) { # user cannot modify the following own permissions for security reasons my $own_forbidden = 0; diff --git a/lib/NGCP/Panel/Role/API/PasswordChange.pm b/lib/NGCP/Panel/Role/API/PasswordChange.pm new file mode 100644 index 0000000000..438dc54e2e --- /dev/null +++ b/lib/NGCP/Panel/Role/API/PasswordChange.pm @@ -0,0 +1,24 @@ +package NGCP::Panel::Role::API::PasswordChange; +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::PasswordChangeAPI", $c); +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/Preferences.pm b/lib/NGCP/Panel/Role/API/Preferences.pm index 04dc677050..9341fdc438 100644 --- a/lib/NGCP/Panel/Role/API/Preferences.pm +++ b/lib/NGCP/Panel/Role/API/Preferences.pm @@ -73,8 +73,6 @@ sub _item_rs { my $item_rs; my $type = $self->container_resource_type; - print "TYPE: $type\n"; - if($type eq "domains") { # we actually return the domain rs here, as we can easily # go to dom_preferences from there diff --git a/lib/NGCP/Panel/Role/API/Subscribers.pm b/lib/NGCP/Panel/Role/API/Subscribers.pm index ca0bc4723e..f1102b663c 100644 --- a/lib/NGCP/Panel/Role/API/Subscribers.pm +++ b/lib/NGCP/Panel/Role/API/Subscribers.pm @@ -407,13 +407,6 @@ sub update_item { my $groupmembers = $full_resource->{groupmembers}; my $prov_subscriber = $subscriber->provisioning_voip_subscriber; - if ($c->stash->{validate_password_change}) { - if (!$resource->{webpassword}) { - $self->error($c, HTTP_FORBIDDEN, "Password expired"); - return; - } - } - $self->process_form_resource($c, $item, $full_resource, $resource, $form); if($subscriber->provisioning_voip_subscriber->is_pbx_pilot && !is_true($resource->{is_pbx_pilot})) { diff --git a/lib/NGCP/Panel/Utils/Form.pm b/lib/NGCP/Panel/Utils/Form.pm index 7bfe536585..bf2b69718d 100644 --- a/lib/NGCP/Panel/Utils/Form.pm +++ b/lib/NGCP/Panel/Utils/Form.pm @@ -27,7 +27,16 @@ sub validate_password { } elsif ($field->name eq 'webpassword') { $is_web_password = 1; } elsif ($field->name eq 'new_password') { - $is_web_password = 1; + if ($pass_change) { + my $ngcp_realm = $c->request->env->{NGCP_REALM} // 'admin'; + if ($ngcp_realm eq 'admin') { + $is_admin_password = 1; + } else { + $is_web_password = 1; + } + } else { + $is_web_password = 1; + } } if ($is_sip_password) { @@ -106,17 +115,19 @@ sub validate_password { $lp_rs = $prov_sub->last_passwords; $check_last_passwords = 1; } - } elsif($field->name eq "webpassword" && $pw->{web_validate}) { + } elsif ($is_web_password) { my $user; my $prov_sub = $c->stash->{subscriber} ? $c->stash->{subscriber}->provisioning_voip_subscriber : undef; if ($pass_change && !$prov_sub) { - $prov_sub = $c->user->provisioning_voip_subscriber; + if ($c->user->can('webpassword')) { + $prov_sub = $c->user; + } } if ($field->form->field('webusername')) { $user = $field->form->field('webusername')->value; - } elsif($prov_sub) { + } elsif ($prov_sub) { $user = $prov_sub->webusername; } if(defined $user && $pass =~ /$user/i) {