View | Details | Raw Unified | Return to bug 20028
Collapse All | Expand All

(-)a/Koha/REST/V1/Patrons/Export.pm (+114 lines)
Line 0 Link Here
1
package Koha::REST::V1::Patrons::Export;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Mojo::Base 'Mojolicious::Controller';
21
22
use Koha::Patrons;
23
24
use Scalar::Util qw(blessed);
25
use Try::Tiny;
26
27
=head1 NAME
28
29
Koha::REST::V1::Patrons::Export
30
31
=head1 API
32
33
=head2 Methods
34
35
=head3 get
36
37
Controller method that gets patron's related data, permission driven
38
39
=cut
40
41
sub get {
42
43
    my $c = shift->openapi->valid_input or return;
44
45
    my $patron = Koha::Patrons->find( $c->validation->param('patron_id') );
46
    my $body   = $c->validation->param('body');
47
48
    return try {
49
50
        unless ($patron) {
51
            return $c->render( status => 404, openapi => {
52
                error => "Patron not found."
53
            } );
54
        }
55
56
        unless ( C4::Context->preference('AllowGDPRPatronExport') ) {
57
            return $c->render(
58
                status  => 403,
59
                openapi => { error => "Configuration prevents patron data export" }
60
            );
61
        }
62
63
        my $export = $patron->export;
64
65
        return $c->render( status => 200, openapi => $export );
66
    }
67
    catch {
68
        $c->unhandled_exception($_);
69
    };
70
}
71
72
=head3 get_public
73
74
Controller method that gets patron's related data, for unprivileged users
75
76
=cut
77
78
sub get_public {
79
80
    my $c = shift->openapi->valid_input or return;
81
82
    my $body      = $c->validation->param('body');
83
    my $patron_id = $c->validation->param('patron_id');
84
85
    return try {
86
87
        unless ( C4::Context->preference('AllowGDPRPatronExport') ) {
88
            return $c->render(
89
                status  => 403,
90
                openapi => { error => "Configuration prevents patron data export" }
91
            );
92
        }
93
94
        my $user = $c->stash('koha.user');
95
96
        unless ( $user->borrowernumber == $patron_id ) {
97
            return $c->render(
98
                status  => 403,
99
                openapi => {
100
                    error => "Accessing other patron's data is forbidden"
101
                }
102
            );
103
        }
104
105
        my $export = $user->export;
106
107
        return $c->render( status => 200, openapi => $export );
108
    }
109
    catch {
110
        $c->unhandled_exception($_);
111
    };
112
}
113
114
1;
(-)a/t/db_dependent/api/v1/patrons_export.t (-2 / +183 lines)
Lines 17-23 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 1;
20
use Test::More tests => 3;
21
21
22
use Test::Mojo;
22
use Test::Mojo;
23
23
Lines 29-36 my $builder = t::lib::TestBuilder->new; Link Here
29
29
30
t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
30
t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
31
31
32
# this is recommended because generation of test data may take a long time and
33
# Mojolicious would otherwise die with "Premature connection close"
34
$ENV{MOJO_INACTIVITY_TIMEOUT} = 120;
35
32
my $t = Test::Mojo->new('Koha::REST::V1');
36
my $t = Test::Mojo->new('Koha::REST::V1');
33
37
38
subtest 'get() tests' => sub {
39
40
    plan tests => 7;
41
42
    $schema->storage->txn_begin;
43
    unauthorized_access_tests('GET', -1, undef);
44
    $schema->storage->txn_rollback;
45
46
    $schema->storage->txn_begin;
47
48
    my $generated = generate_test_data();
49
    my $patron = $generated->{patron};
50
    my $test_objects = $generated->{test_objects};
51
52
    my $password = '12345';
53
    my $librarian = $builder->build_object({
54
            class => 'Koha::Patrons',
55
            value => { flags => 2**4 }    # borrowers flag = 4
56
    });
57
    $librarian->set_password( { password => $password, skip_validation => 1 } );
58
    my $userid = $librarian->userid;
59
60
    t::lib::Mocks::mock_preference( 'AllowGDPRPatronExport', 1 );
61
    my $ret = $t->get_ok("//$userid:$password@/api/v1/patrons/" . $patron->id
62
                         . '/export')
63
      ->status_is(200)
64
      ->json_is($test_objects);
65
66
    t::lib::Mocks::mock_preference( 'AllowGDPRPatronExport', 0 );
67
    $ret = $t->get_ok("//$userid:$password@/api/v1/patrons/"
68
                         . $patron->id . '/export')
69
      ->status_is(403)
70
      ->json_is('/error', 'Configuration prevents patron data export');
71
72
    $schema->storage->txn_rollback;
73
};
74
75
subtest 'get_public() tests' => sub {
76
77
    plan tests => 7;
78
79
    $schema->storage->txn_begin;
80
    unauthorized_access_tests('GET', -1, undef, 1);
81
    $schema->storage->txn_rollback;
82
83
    $schema->storage->txn_begin;
84
85
    my $generated = generate_test_data();
86
    my $patron = $generated->{patron};
87
    my $test_objects = $generated->{test_objects};
88
    my $userid = $patron->userid;
89
    my $password = $patron->{cleartext_password};
90
91
    t::lib::Mocks::mock_preference( 'AllowGDPRPatronExport', 1 );
92
    my $ret = $t->get_ok("//$userid:$password@/api/v1/public/patrons/"
93
                         . $patron->id . '/export')
94
      ->status_is(200)
95
      ->json_is($test_objects);
96
97
    t::lib::Mocks::mock_preference( 'AllowGDPRPatronExport', 0 );
98
    $ret = $t->get_ok("//$userid:$password@/api/v1/public/patrons/"
99
                         . $patron->id . '/export')
100
      ->status_is(403)
101
      ->json_is('/error', 'Configuration prevents patron data export');
102
103
    $schema->storage->txn_rollback;
104
};
105
34
subtest 'validate patrons_export OpenAPI definition' => sub {
106
subtest 'validate patrons_export OpenAPI definition' => sub {
35
107
36
    my @related_sources = _get_related_sources();
108
    my @related_sources = _get_related_sources();
Lines 78-80 sub _get_related_sources { Link Here
78
    my @sorted = sort keys %$sources;
150
    my @sorted = sort keys %$sources;
79
    return @sorted;
151
    return @sorted;
80
}
152
}
81
- 
153
154
sub generate_test_data {
155
    my @related_sources = _get_related_sources();
156
    my $patron = $builder->build_object(
157
        {
158
            class => 'Koha::Patrons',
159
        }
160
    );
161
162
    my $password = '12345';
163
    $patron->set_password( { password => $password, skip_validation => 1 } );
164
    $patron->discard_changes;
165
    $patron->{cleartext_password} = $password;
166
    my $test_objects = {
167
        'Borrower' => $patron->unblessed,
168
    };
169
170
    my $limit_data     = 5; # build this many random test objects
171
    my $generated_data = 0;
172
173
    foreach my $src ( @related_sources ) {
174
        next if $src eq 'Borrower'; # Borrower is a hashref,
175
        $test_objects->{$src} = []; # the others are arrayrefs
176
    }
177
178
    my $result_source = Koha::Patron->new->_result()->result_source;
179
    foreach my $rel (   Koha::Patron->new->_result()->relationships() ) {
180
        if ($generated_data >= $limit_data) {
181
            last;
182
        }
183
184
        my $related_source = $result_source->related_source( $rel );
185
        my $source_name = $related_source->source_name;
186
187
                my $info = $result_source->relationship_info( $rel );
188
189
        # We are not interested in the "belongs_to" relationship of borrowers.
190
        # These are tables like branches, categories and sms_provider.
191
        if ( $info->{'attrs'}->{'is_depends_on'} ) {
192
            next;
193
        }
194
195
        ( my $rel_col = (keys %{$info->{'cond'}})[0] ) =~ s/^foreign\.//;
196
197
        # Generate test data into related tables
198
        my $built;
199
        if ( $related_source->result_class->can('koha_objects_class') ) {
200
            $built = $builder->build_object(
201
                {
202
                    class => $related_source->result_class->koha_objects_class,
203
                    value => { $rel_col => $patron->borrowernumber }
204
                }
205
            );
206
            $built = $built->unblessed;
207
        } else {
208
            $built = $builder->build(
209
                {
210
                    source => $source_name,
211
                    value  => { $rel_col => $patron->borrowernumber }
212
                }
213
            );
214
        }
215
        push @{ $test_objects->{$related_source->source_name} }, $built;
216
        $generated_data++;
217
    }
218
219
    # Generate test data into a random table. Just one because test data
220
    # generation takes so long.
221
    $builder->build( {
222
        source => (keys %$test_objects)[rand keys %$test_objects]
223
    } );
224
225
    return {
226
        patron => $patron,
227
        test_objects => $test_objects
228
    };
229
}
230
231
# Centralized tests for 401s and 403s assuming the endpoint requires
232
# borrowers flag for access
233
sub unauthorized_access_tests {
234
    my ($verb, $patron_id, $json, $public) = @_;
235
236
    my $endpoint = '/api/v1/' . ( $public ? 'public/' : '' ) . 'patrons';
237
    $endpoint .= ($patron_id) ? "/$patron_id/export" : '';
238
239
    subtest 'unauthorized access tests' => sub {
240
        plan tests => 5;
241
242
        my $verb_ok = lc($verb) . '_ok';
243
244
        $t->$verb_ok($endpoint => json => $json)
245
          ->status_is(401);
246
247
        my $unauthorized_patron = $builder->build_object(
248
            {
249
                class => 'Koha::Patrons',
250
                value => { flags => 0 }
251
            }
252
        );
253
        my $password = "12345";
254
        $unauthorized_patron->set_password(
255
            { password => $password, skip_validation => 1 } );
256
        my $unauth_userid = $unauthorized_patron->userid;
257
258
        $t->$verb_ok( "//$unauth_userid:$password\@$endpoint" => json => $json )
259
          ->status_is(403)
260
          ->json_has('/required_permissions');
261
    };
262
}

Return to bug 20028