MT#13201 Enhance voucher API.

- Use billing_data ACL grants to modify vouchers
- Use encryption in UI for voucher code

Change-Id: I7711a43db8596d5f733d6c52d2f6608f434b2463
changes/12/1812/6
Andreas Granig 10 years ago
parent 8cb165b3fc
commit 4227fd2522

@ -138,6 +138,12 @@ sub OPTIONS :Allow {
sub POST :Allow {
my ($self, $c) = @_;
unless($c->user->billing_data) {
$c->log->error("user does not have billing data rights");
$self->error($c, HTTP_FORBIDDEN, "Unsufficient rights to create voucher");
return;
}
my $guard = $c->model('DB')->txn_scope_guard;
{
my $resource = $self->get_valid_post_data(
@ -158,7 +164,7 @@ sub POST :Allow {
}
my $item;
my $code = $self->encrypt_code($c, $resource->{code});
my $code = NGCP::Panel::Utils::Voucher::encrypt_code($c, $resource->{code});
$item = $c->model('DB')->resultset('vouchers')->find({
reseller_id => $resource->{reseller_id},
code => $code,

@ -84,6 +84,11 @@ sub OPTIONS :Allow {
sub PATCH :Allow {
my ($self, $c, $id) = @_;
unless($c->user->billing_data) {
$c->log->error("user does not have billing data rights");
$self->error($c, HTTP_FORBIDDEN, "Unsufficient rights to edit voucher");
return;
}
my $guard = $c->model('DB')->txn_scope_guard;
{
my $preference = $self->require_preference($c);
@ -127,6 +132,11 @@ sub PATCH :Allow {
sub PUT :Allow {
my ($self, $c, $id) = @_;
unless($c->user->billing_data) {
$c->log->error("user does not have billing data rights");
$self->error($c, HTTP_FORBIDDEN, "Unsufficient rights to edit voucher");
return;
}
my $guard = $c->model('DB')->txn_scope_guard;
{
my $preference = $self->require_preference($c);
@ -167,7 +177,11 @@ sub PUT :Allow {
sub DELETE :Allow {
my ($self, $c, $id) = @_;
unless($c->user->billing_data) {
$c->log->error("user does not have billing data rights");
$self->error($c, HTTP_FORBIDDEN, "Unsufficient rights to delete voucher");
return;
}
my $guard = $c->model('DB')->txn_scope_guard;
{
my $item = $self->item_by_id($c, $id);

@ -38,6 +38,7 @@ sub list_admin :PathPart('administrator') :Chained('/') :CaptureArgs(0) {
{ name => "read_only", title => $c->loc("Read Only") },
{ name => "show_passwords", title => $c->loc("Show Passwords") },
{ name => "call_data", title => $c->loc("Show CDRs") },
{ name => "billing_data", title => $c->loc("Show Billing Info") },
);
if($c->user->is_superuser) {
@{ $cols } = (@{ $cols }, { name => "lawful_intercept", title => $c->loc("Lawful Intercept") });

@ -146,6 +146,7 @@ sub base :Chained('list_reseller') :PathPart('') :CaptureArgs(1) {
{ name => "read_only", title => $c->loc('Read-Only') },
{ name => "show_passwords", title => $c->loc('Show Passwords') },
{ name => "call_data", title => $c->loc('Show CDRs') },
{ name => "billing_data", title => $c->loc('Show Billing Info') },
]);
$c->stash->{customer_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", search => 1, title => $c->loc('#') },

@ -12,6 +12,8 @@ use NGCP::Panel::Utils::Message;
use NGCP::Panel::Utils::Navigation;
use NGCP::Panel::Utils::Datatables;
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Voucher;
use Scalar::Util qw/blessed/;
sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) {
my ($self, $c) = @_;
@ -34,9 +36,10 @@ sub voucher_list :Chained('/') :PathPart('voucher') :CaptureArgs(0) {
});
}
$c->stash(voucher_rs => $voucher_rs);
$c->stash->{voucher_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", "search" => 1, "title" => $c->loc("#") },
{ name => "code", "search" => 1, "title" => $c->loc("Code") },
$c->user->billing_data ? { name => "code", "search" => 1, "title" => $c->loc("Code") } : (),
{ name => "amount", "search" => 1, "title" => $c->loc("Amount") },
{ name => "reseller.name", "search" => 1, "title" => $c->loc("Reseller") },
{ name => "valid_until", "search" => 1, "title" => $c->loc("Valid Until") },
@ -55,7 +58,21 @@ sub ajax :Chained('voucher_list') :PathPart('ajax') :Args(0) {
my ($self, $c) = @_;
my $resultset = $c->stash->{voucher_rs};
NGCP::Panel::Utils::Datatables::process($c, $resultset, $c->stash->{voucher_dt_columns});
NGCP::Panel::Utils::Datatables::process($c, $resultset, $c->stash->{voucher_dt_columns}, sub {
my $row = shift;
my $v = { $row->get_inflated_columns };
if($c->user->billing_data) {
$v->{code} = NGCP::Panel::Utils::Voucher::decrypt_code($c, $row->code);
} else {
$v->{code} = "<retracted>";
}
foreach my $k(keys %{ $v }) {
if(blessed($v->{$k}) && $v->{$k}->isa('DateTime')) {
$v->{$k} = NGCP::Panel::Utils::DateTime::to_string($v->{$k});
}
}
return %{ $v };
});
$c->detach( $c->view("JSON") );
}
@ -90,6 +107,9 @@ sub base :Chained('voucher_list') :PathPart('') :CaptureArgs(1) {
sub delete :Chained('base') :PathPart('delete') {
my ($self, $c) = @_;
$c->detach('/denied_page')
unless($c->user->billing_data);
try {
$c->stash->{voucher_result}->delete;
NGCP::Panel::Utils::Message->info(
@ -111,16 +131,24 @@ sub delete :Chained('base') :PathPart('delete') {
sub edit :Chained('base') :PathPart('edit') {
my ($self, $c) = @_;
$c->detach('/denied_page')
unless($c->user->billing_data);
my $posted = ($c->request->method eq 'POST');
my $form;
my $params = $c->stash->{voucher};
$params->{valid_until} =~ s/^(\d{4}\-\d{2}\-\d{2}).*$/$1/;
$params->{reseller}{id} = delete $params->{reseller_id};
if($c->user->billing_data) {
$params->{code} = NGCP::Panel::Utils::Voucher::decrypt_code($c, $params->{code});
} else {
delete $params->{code};
}
$params = $params->merge($c->session->{created_objects});
if($c->user->is_superuser) {
$form = NGCP::Panel::Form::Voucher::Admin->new;
$form = NGCP::Panel::Form::Voucher::Admin->new(ctx => $c);
} else {
$form = NGCP::Panel::Form::Voucher::Reseller->new;
$form = NGCP::Panel::Form::Voucher::Reseller->new(ctx => $c);
}
$form->process(
posted => $posted,
@ -151,6 +179,11 @@ sub edit :Chained('base') :PathPart('edit') {
->add(days => 1)->subtract(seconds => 1);
}
if($c->user->billing_data) {
$form->values->{code} = NGCP::Panel::Utils::Voucher::encrypt_code($c, $form->values->{code});
} else {
delete $form->values->{code};
}
my $schema = $c->model('DB');
$schema->txn_do(sub {
$c->stash->{voucher_result}->update($form->values);
@ -178,15 +211,18 @@ sub edit :Chained('base') :PathPart('edit') {
sub create :Chained('voucher_list') :PathPart('create') :Args(0) {
my ($self, $c) = @_;
$c->detach('/denied_page')
unless($c->user->billing_data);
my $posted = ($c->request->method eq 'POST');
my $form;
my $params = {};
$params->{reseller}{id} = delete $params->{reseller_id};
$params = $params->merge($c->session->{created_objects});
if($c->user->is_superuser) {
$form = NGCP::Panel::Form::Voucher::Admin->new;
$form = NGCP::Panel::Form::Voucher::Admin->new(ctx => $c);
} else {
$form = NGCP::Panel::Form::Voucher::Reseller->new;
$form = NGCP::Panel::Form::Voucher::Reseller->new(ctx => $c);
}
$form->process(
posted => $posted,
@ -217,6 +253,7 @@ sub create :Chained('voucher_list') :PathPart('create') :Args(0) {
$form->values->{valid_until} = NGCP::Panel::Utils::DateTime::from_string($form->values->{valid_until})
->add(days => 1)->subtract(seconds => 1);
}
$form->values->{code} = NGCP::Panel::Utils::Voucher::encrypt_code($c, $form->values->{code});
my $voucher = $c->model('DB')->resultset('vouchers')->create($form->values);
$c->session->{created_objects}->{voucher} = { id => $voucher->id };
delete $c->session->{created_objects}->{reseller};
@ -241,6 +278,9 @@ sub create :Chained('voucher_list') :PathPart('create') :Args(0) {
sub voucher_upload :Chained('voucher_list') :PathPart('upload') :Args(0) {
my ($self, $c) = @_;
$c->detach('/denied_page')
unless($c->user->billing_data);
my $form = NGCP::Panel::Form::Voucher::Upload->new;
my $upload = $c->req->upload('upload_vouchers');
my $posted = $c->req->method eq 'POST';
@ -281,6 +321,7 @@ sub voucher_upload :Chained('voucher_list') :PathPart('upload') :Args(0) {
while(my $row = $csv->getline_hr($upload->fh)) {
++$linenum;
use Data::Printer; p $row;
if($csv->is_missing(1)) {
push @fails, $linenum;
next;
@ -290,6 +331,7 @@ sub voucher_upload :Chained('voucher_list') :PathPart('upload') :Args(0) {
->add(days => 1)->subtract(seconds => 1);
}
$row->{customer_id} = undef if(defined $row->{customer_id} && $row->{customer_id} eq "");
$row->{code} = NGCP::Panel::Utils::Voucher::encrypt_code($c, $row->{code});
push @vouchers, $row;
}
unless ($csv->eof()) {

@ -16,7 +16,7 @@ has_block 'fields' => (
tag => 'div',
class => [qw(modal-body)],
render_list => [qw(
reseller login md5pass is_superuser is_master is_active read_only show_passwords call_data lawful_intercept
reseller login md5pass is_superuser is_master is_active read_only show_passwords call_data billing_data lawful_intercept
)],
);

@ -12,7 +12,7 @@ sub build_form_element_class {[qw(form-horizontal)]}
has_field 'login' => (type => 'Text', required => 1, minlength => 5);
has_field 'md5pass' => (type => 'Password', required => 1, label => 'Password');
for (qw(is_active show_passwords call_data)) {
for (qw(is_active show_passwords call_data billing_data)) {
has_field $_ => (type => 'Boolean', default => 1);
}
for (qw(is_master read_only)) {
@ -23,7 +23,7 @@ has_block 'fields' => (
tag => 'div',
class => [qw(modal-body)],
render_list => [qw(
login md5pass is_master is_active read_only show_passwords call_data
login md5pass is_master is_active read_only show_passwords call_data billing_data
)],
);
has_block 'actions' => (tag => 'div', class => [qw(modal-footer)], render_list => [qw(save)],);

@ -81,5 +81,15 @@ sub validate_valid_until {
}
}
sub update_fields {
my $self = shift;
my $c = $self->ctx;
return unless $c;
unless($c->user->billing_data) {
$self->field('code')->inactive(1);
}
}
1
# vim: set tabstop=4 expandtab:

@ -55,7 +55,7 @@ has_field 'customer' => (
sub validate_valid_until {
my ($self, $field) = @_;
unless($field->value =~ /^(\d{4})\-\d{2}\-\d{2}(T| )\d{2}:\d{2}:\d{2}$/) {
unless($field->value =~ /^(\d{4})\-\d{2}\-\d{2}(T| )\d{2}:\d{2}:\d{2}(\+\d{2}:\d{2})?$/) {
my $err_msg = 'Invalid date format, must be YYYY-MM-DD';
$field->add_error($err_msg);
}
@ -65,5 +65,15 @@ sub validate_valid_until {
}
}
sub update_fields {
my $self = shift;
my $c = $self->ctx;
return unless $c;
unless($c->user->billing_data) {
$self->field('code')->inactive(1);
}
}
1
# vim: set tabstop=4 expandtab:

@ -1,8 +1,6 @@
package NGCP::Panel::Role::API::Vouchers;
use Moose::Role;
use Sipwise::Base;
use Crypt::Rijndael;
use MIME::Base64;
with 'NGCP::Panel::Role::API' => {
-alias =>{ item_rs => '_item_rs', },
-excludes => [ 'item_rs' ],
@ -15,6 +13,7 @@ use Data::HAL::Link qw();
use HTTP::Status qw(:constants);
use NGCP::Panel::Form::Voucher::AdminAPI;
use NGCP::Panel::Form::Voucher::ResellerAPI;
use NGCP::Panel::Utils::Voucher;
sub item_rs {
my ($self, $c) = @_;
@ -66,7 +65,11 @@ sub hal_from_item {
);
$resource{valid_until} = $item->valid_until->ymd('-') . ' ' . $item->valid_until->hms(':');
$resource{code} = $self->decrypt_code($c, $item->code);
if($c->user->billing_data) {
$resource{code} = NGCP::Panel::Utils::Voucher::decrypt_code($c, $item->code);
} else {
delete $resource{code};
}
$resource{id} = int($item->id);
$hal->resource({%resource});
return $hal;
@ -78,44 +81,6 @@ sub item_by_id {
return $item_rs->find($id);
}
sub encrypt_code {
my ($self, $c, $plain) = @_;
my $key = $c->config->{vouchers}->{key};
my $iv = $c->config->{vouchers}->{iv};
# pkcs#5 padding to 16 bytes blocksize
my $pad = 16 - (length $plain) % 16;
$plain .= pack('C', $pad) x $pad;
my $cipher = Crypt::Rijndael->new(
$key,
Crypt::Rijndael::MODE_CBC()
);
$cipher->set_iv($iv);
my $crypted = $cipher->encrypt($plain);
my $b64 = encode_base64($crypted, '');
return $b64;
}
sub decrypt_code {
my ($self, $c, $code) = @_;
my $key = $c->config->{vouchers}->{key};
my $iv = $c->config->{vouchers}->{iv};
my $cipher = Crypt::Rijndael->new(
$key,
Crypt::Rijndael::MODE_CBC()
);
$cipher->set_iv($iv);
my $crypted = decode_base64($code);
my $plain = $cipher->decrypt($crypted) . "";
# remove padding
$plain =~ s/[\x01-\x1e]*$//;
return $plain;
}
sub update_item {
my ($self, $c, $item, $old_resource, $resource, $form) = @_;
@ -130,7 +95,7 @@ sub update_item {
$resource->{reseller_id} = $c->user->reseller_id;
}
my $code = $self->encrypt_code($c, $resource->{code});
my $code = NGCP::Panel::Utils::Voucher::encrypt_code($c, $resource->{code});
my $dup_item = $c->model('DB')->resultset('vouchers')->find({
reseller_id => $resource->{reseller_id},
code => $code,

@ -175,6 +175,7 @@ sub process {
for my $row ($rs->all) {
push @{ $aaData }, _prune_row($cols, $row->get_inflated_columns);
if (defined $row_func) {
my $r = $row_func->($row);
$aaData->[-1]->put($row_func->($row));
}
}

@ -56,6 +56,15 @@ sub sec_to_hms
return "$h:$m:$s";
}
sub to_string
{
my ($dt) = @_;
return unless defined ($dt);
my $s = $dt->ymd('-') . ' ' . $dt->hms(':');
$s .= '.'.$dt->millisecond if $dt->millisecond > 0.0;
return $s;
}
1;

@ -0,0 +1,44 @@
package NGCP::Panel::Utils::Voucher;
use Sipwise::Base;
use Crypt::Rijndael;
use MIME::Base64;
sub encrypt_code {
my ($c, $plain) = @_;
my $key = $c->config->{vouchers}->{key};
my $iv = $c->config->{vouchers}->{iv};
# pkcs#5 padding to 16 bytes blocksize
my $pad = 16 - (length $plain) % 16;
$plain .= pack('C', $pad) x $pad;
my $cipher = Crypt::Rijndael->new(
$key,
Crypt::Rijndael::MODE_CBC()
);
$cipher->set_iv($iv);
my $crypted = $cipher->encrypt($plain);
my $b64 = encode_base64($crypted, '');
return $b64;
}
sub decrypt_code {
my ($c, $code) = @_;
my $key = $c->config->{vouchers}->{key};
my $iv = $c->config->{vouchers}->{iv};
my $cipher = Crypt::Rijndael->new(
$key,
Crypt::Rijndael::MODE_CBC()
);
$cipher->set_iv($iv);
my $crypted = decode_base64($code);
my $plain = $cipher->decrypt($crypted) . "";
# remove padding
$plain =~ s/[\x01-\x1e]*$//;
return $plain;
}
1;

@ -13,6 +13,7 @@
helper.ajax_uri = c.uri_for( c.controller.action_for('ajax') );
UNLESS c.user.read_only;
IF c.user.billing_data;
helper.dt_buttons = [
{ name = c.loc('Edit'), uri = "/voucher/'+full[\"id\"]+'/edit", class = 'btn-small btn-primary', icon = 'icon-edit' },
{ name = c.loc('Delete'), uri = "/voucher/'+full[\"id\"]+'/delete", class = 'btn-small btn-secondary', icon = 'icon-trash' },
@ -22,6 +23,7 @@
{ name = c.loc('Upload Vouchers as CSV'), uri = c.uri_for('/voucher/upload'), icon = 'icon-star' },
];
END;
END;
PROCESS 'helpers/datatables.tt';
-%]

@ -88,7 +88,6 @@ sub test_voucher {
reseller_id => $default_reseller_id,
valid_until => '2037-01-01 12:00:00',
};
p $voucher;
$req = HTTP::Request->new('POST', $uri.'/api/vouchers/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json($voucher));
@ -118,7 +117,21 @@ sub test_voucher {
$voucher_id = delete $put_voucher->{id};
is_deeply($voucher, $put_voucher, "check PUTed voucher against POSTed voucher");
p $put_voucher;
$req = HTTP::Request->new('PATCH', $voucher_uri);
$req->header('Content-Type' => 'application/json-patch+json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json([{op=>"replace", path=>"/code", value=>$put_voucher->{code}}]));
$res = $ua->request($req);
is($res->code, 200, _get_request_test_message("PATCH test voucher"));
$req = HTTP::Request->new('GET', $voucher_uri);
$res = $ua->request($req);
is($res->code, 200, _get_request_test_message("fetch PATCH test voucher"));
my $patch_voucher = JSON::from_json($res->decoded_content);
delete $patch_voucher->{_links};
$voucher_id = delete $patch_voucher->{id};
is_deeply($voucher, $patch_voucher, "check PATCHed voucher against POSTed voucher");
$req = HTTP::Request->new('POST', $uri.'/api/vouchers/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json($put_voucher));

Loading…
Cancel
Save