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

(-)a/C4/Installer/PerlDependencies.pm (-4 / +9 lines)
Lines 874-883 our $PERL_DEPS = { Link Here
874
        # also required for Zebra installs: about page: bug 20061
874
        # also required for Zebra installs: about page: bug 20061
875
    },
875
    },
876
    'Net::OAuth2::AuthorizationServer' => {
876
    'Net::OAuth2::AuthorizationServer' => {
877
        'usage'    => 'REST API',
877
        usage    => 'REST API',
878
        'required' => '1',
878
        required => '1',
879
        'min_ver' => '0.16',
879
        min_ver  => '0.16',
880
    },
880
    },
881
    'Mojolicious::Plugin::OAuth2::Server' => {
882
        usage    => 'REST API',
883
        required => '1',
884
        min_ver  => '0.40',
885
    }
881
886
882
};
887
};
883
888
(-)a/Koha/OAuth.pm (-29 / +9 lines)
Lines 1-25 Link Here
1
package Koha::OAuth;
1
package Koha::OAuth;
2
2
3
use Modern::Perl;
3
use Modern::Perl;
4
use Net::OAuth2::AuthorizationServer::ClientCredentialsGrant;
5
use Koha::OAuthAccessTokens;
4
use Koha::OAuthAccessTokens;
6
use Koha::OAuthAccessToken;
5
use Koha::OAuthAccessToken;
7
6
8
sub grant {
7
sub config {
9
    my $grant = Net::OAuth2::AuthorizationServer::ClientCredentialsGrant->new(
8
    return {
10
        verify_client_cb => \&_verify_client_cb,
9
        verify_client_cb => \&_verify_client_cb,
11
        store_access_token_cb => \&_store_access_token_cb,
10
        store_access_token_cb => \&_store_access_token_cb,
12
        verify_access_token_cb => \&_verify_access_token_cb
11
        verify_access_token_cb => \&_verify_access_token_cb
13
    );
12
    };
14
15
    return $grant;
16
}
13
}
17
14
18
sub _verify_client_cb {
15
sub _verify_client_cb {
19
    my (%args) = @_;
16
    my (%args) = @_;
20
17
21
    my ($client_id, $scopes_ref, $client_secret)
18
    my ($client_id, $client_secret)
22
        = @args{ qw/ client_id scopes client_secret / };
19
        = @args{ qw/ client_id client_secret / };
23
20
24
    return (0, 'unauthorized_client') unless $client_id;
21
    return (0, 'unauthorized_client') unless $client_id;
25
22
Lines 30-57 sub _verify_client_cb { Link Here
30
27
31
    return (0, 'access_denied') unless $client_secret eq $client->{client_secret};
28
    return (0, 'access_denied') unless $client_secret eq $client->{client_secret};
32
29
33
    $client->{scope} //= [];
30
    return (1, undef, []);
34
    $client->{scope} = [ $client->{scope} ] if ref $client->{scope} ne 'ARRAY';
35
    my $client_scopes = [];
36
    foreach my $scope ( @{ $scopes_ref // [] } ) {
37
        if (!grep { $_ eq $scope } @{ $client->{scope} }) {
38
            return (0, 'invalid_scope');
39
        }
40
        push @$client_scopes, $scope;
41
    }
42
43
    return (1, undef, $client_scopes);
44
}
31
}
45
32
46
sub _store_access_token_cb {
33
sub _store_access_token_cb {
47
    my ( %args ) = @_;
34
    my ( %args ) = @_;
48
35
49
    my ( $client_id, $access_token, $expires_in, $scopes_ref )
36
    my ( $client_id, $access_token, $expires_in )
50
        = @args{ qw/ client_id access_token expires_in scopes / };
37
        = @args{ qw/ client_id access_token expires_in / };
51
38
52
    my $at = Koha::OAuthAccessToken->new({
39
    my $at = Koha::OAuthAccessToken->new({
53
        access_token  => $access_token,
40
        access_token  => $access_token,
54
        scope         => join (' ', @$scopes_ref),
55
        expires       => time + $expires_in,
41
        expires       => time + $expires_in,
56
        client_id     => $client_id,
42
        client_id     => $client_id,
57
    });
43
    });
Lines 63-69 sub _store_access_token_cb { Link Here
63
sub _verify_access_token_cb {
49
sub _verify_access_token_cb {
64
    my (%args) = @_;
50
    my (%args) = @_;
65
51
66
    my ($access_token, $scopes_ref) = @args{qw(access_token scopes)};
52
    my $access_token = $args{access_token};
67
53
68
    my $at = Koha::OAuthAccessTokens->find($access_token);
54
    my $at = Koha::OAuthAccessTokens->find($access_token);
69
    if ($at) {
55
    if ($at) {
Lines 72-83 sub _verify_access_token_cb { Link Here
72
            $at->delete;
58
            $at->delete;
73
59
74
            return (0, 'invalid_grant')
60
            return (0, 'invalid_grant')
75
        } elsif ( $scopes_ref ) {
76
            foreach my $scope ( @{ $scopes_ref // [] } ) {
77
                unless ($at->has_scope($scope)) {
78
                    return (0, 'invalid_grant');
79
                }
80
            }
81
        }
61
        }
82
62
83
        return $at->unblessed;
63
        return $at->unblessed;
(-)a/Koha/OAuthAccessToken.pm (-8 lines)
Lines 4-17 use Modern::Perl; Link Here
4
4
5
use base qw(Koha::Object);
5
use base qw(Koha::Object);
6
6
7
sub has_scope {
8
    my ($self, $scope) = @_;
9
10
    my @scopes = split / /, $self->scope;
11
12
    return scalar grep { $_ eq $scope } @scopes;
13
}
14
15
sub _type {
7
sub _type {
16
    return 'OauthAccessToken';
8
    return 'OauthAccessToken';
17
}
9
}
(-)a/Koha/REST/V1.pm (+3 lines)
Lines 19-24 use Modern::Perl; Link Here
19
19
20
use Mojo::Base 'Mojolicious';
20
use Mojo::Base 'Mojolicious';
21
21
22
use Koha::OAuth;
23
22
use C4::Context;
24
use C4::Context;
23
25
24
=head1 NAME
26
=head1 NAME
Lines 51-56 sub startup { Link Here
51
        $self->secrets([$secret_passphrase]);
53
        $self->secrets([$secret_passphrase]);
52
    }
54
    }
53
55
56
    $self->plugin('OAuth2::Server' => Koha::OAuth::config);
54
    $self->plugin(OpenAPI => {
57
    $self->plugin(OpenAPI => {
55
        url => $self->home->rel_file("api/v1/swagger/swagger.json"),
58
        url => $self->home->rel_file("api/v1/swagger/swagger.json"),
56
        route => $self->routes->under('/api/v1')->to('Auth#under'),
59
        route => $self->routes->under('/api/v1')->to('Auth#under'),
(-)a/Koha/REST/V1/Auth.pm (-15 / +21 lines)
Lines 22-27 use Modern::Perl; Link Here
22
use Mojo::Base 'Mojolicious::Controller';
22
use Mojo::Base 'Mojolicious::Controller';
23
23
24
use C4::Auth qw( check_cookie_auth get_session haspermission );
24
use C4::Auth qw( check_cookie_auth get_session haspermission );
25
use C4::Context;
25
26
26
use Koha::Account::Lines;
27
use Koha::Account::Lines;
27
use Koha::Checkouts;
28
use Koha::Checkouts;
Lines 110-136 sub authenticate_api_request { Link Here
110
    my ( $c ) = @_;
111
    my ( $c ) = @_;
111
112
112
    my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
113
    my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
114
    my $authorization = $spec->{'x-koha-authorization'};
113
115
114
    my $authorization_header = $c->req->headers->authorization;
116
    if (my $oauth = $c->oauth) {
115
    if ($authorization_header) {
117
        my $clients = C4::Context->config('api_client');
116
        my $grant = Koha::OAuth->grant;
118
        $clients = [ $clients ] unless ref $clients eq 'ARRAY';
117
        my ($type, $token) = split / /, $authorization_header;
119
        my ($client) = grep { $_->{client_id} eq $oauth->{client_id} } @$clients;
118
        my ($is_valid, $error) = $grant->verify_access_token(
119
            access_token => $token,
120
            scopes => $spec->{'x-koha-scopes'} // [],
121
        );
122
120
123
        if (!$is_valid) {
121
        my $patron = Koha::Patrons->find($client->{patron_id});
124
            Koha::Exceptions::Authorization::Unauthorized->throw(
122
        my $permissions = $authorization->{'permissions'};
125
                error => $error,
123
        # Check if the patron is authorized
126
                required_permissions => $spec->{'x-koha-scopes'},
124
        if ( haspermission($patron->userid, $permissions)
127
            );
125
            or allow_owner($c, $authorization, $patron)
126
            or allow_guarantor($c, $authorization, $patron) ) {
127
128
            validate_query_parameters( $c, $spec );
129
130
            # Everything is ok
131
            return 1;
128
        }
132
        }
129
133
130
        return 1;
134
        Koha::Exceptions::Authorization::Unauthorized->throw(
135
            error => "Authorization failure. Missing required permission(s).",
136
            required_permissions => $permissions,
137
        );
131
    }
138
    }
132
139
133
    my $authorization = $spec->{'x-koha-authorization'};
134
    my $cookie = $c->cookie('CGISESSID');
140
    my $cookie = $c->cookie('CGISESSID');
135
    my ($session, $user);
141
    my ($session, $user);
136
    # Mojo doesn't use %ENV the way CGI apps do
142
    # Mojo doesn't use %ENV the way CGI apps do
(-)a/Koha/REST/V1/OAuth.pm (-16 / +14 lines)
Lines 3-8 package Koha::REST::V1::OAuth; Link Here
3
use Modern::Perl;
3
use Modern::Perl;
4
4
5
use Mojo::Base 'Mojolicious::Controller';
5
use Mojo::Base 'Mojolicious::Controller';
6
7
use Net::OAuth2::AuthorizationServer;
6
use Koha::OAuth;
8
use Koha::OAuth;
7
9
8
use C4::Context;
10
use C4::Context;
Lines 17-31 sub token { Link Here
17
19
18
    my $client_id = $c->validation->param('client_id');
20
    my $client_id = $c->validation->param('client_id');
19
    my $client_secret = $c->validation->param('client_secret');
21
    my $client_secret = $c->validation->param('client_secret');
20
    my $scope = [ split / /, ($c->validation->param('scope') // '') ];
21
22
22
    my $grant = Koha::OAuth->grant;
23
    my $cb = "${grant_type}_grant";
24
    my $server = Net::OAuth2::AuthorizationServer->new;
25
    my $grant = $server->$cb(Koha::OAuth::config);
23
26
24
    # verify a client against known clients
27
    # verify a client against known clients
25
    my ( $is_valid, $error, $scopes ) = $grant->verify_client(
28
    my ( $is_valid, $error ) = $grant->verify_client(
26
      client_id     => $client_id,
29
        client_id     => $client_id,
27
      client_secret => $client_secret,
30
        client_secret => $client_secret,
28
      scopes        => $scope,
29
    );
31
    );
30
32
31
    unless ($is_valid) {
33
    unless ($is_valid) {
Lines 34-56 sub token { Link Here
34
36
35
    # generate a token
37
    # generate a token
36
    my $token = $grant->token(
38
    my $token = $grant->token(
37
      client_id       => $client_id,
39
        client_id => $client_id,
38
      scopes          => $scopes,
40
        type      => 'access',
39
      type            => 'access',
40
    );
41
    );
41
42
42
    # store access token
43
    # store access token
43
    my $expires_in = 3600;
44
    my $expires_in = 3600;
44
    $grant->store_access_token(
45
    $grant->store_access_token(
45
        client_id         => $client_id,
46
        client_id    => $client_id,
46
        access_token      => $token,
47
        access_token => $token,
47
        expires_in        => $expires_in,
48
        expires_in   => $expires_in,
48
        scopes => $scopes,
49
    );
49
    );
50
50
51
    my $at = Koha::OAuthAccessTokens->search({
51
    my $at = Koha::OAuthAccessTokens->search({ access_token => $token })->next;
52
        access_token => $token,
53
    })->next;
54
52
55
    my $response = {
53
    my $response = {
56
        access_token => $token,
54
        access_token => $token,
(-)a/Koha/Schema/Result/OauthAccessToken.pm (-9 / +2 lines)
Lines 35-45 __PACKAGE__->table("oauth_access_tokens"); Link Here
35
  is_nullable: 0
35
  is_nullable: 0
36
  size: 255
36
  size: 255
37
37
38
=head2 scope
39
40
  data_type: 'text'
41
  is_nullable: 1
42
43
=head2 expires
38
=head2 expires
44
39
45
  data_type: 'integer'
40
  data_type: 'integer'
Lines 52-59 __PACKAGE__->add_columns( Link Here
52
  { data_type => "varchar", is_nullable => 0, size => 255 },
47
  { data_type => "varchar", is_nullable => 0, size => 255 },
53
  "client_id",
48
  "client_id",
54
  { data_type => "varchar", is_nullable => 0, size => 255 },
49
  { data_type => "varchar", is_nullable => 0, size => 255 },
55
  "scope",
56
  { data_type => "text", is_nullable => 1 },
57
  "expires",
50
  "expires",
58
  { data_type => "integer", is_nullable => 0 },
51
  { data_type => "integer", is_nullable => 0 },
59
);
52
);
Lines 71-78 __PACKAGE__->add_columns( Link Here
71
__PACKAGE__->set_primary_key("access_token");
64
__PACKAGE__->set_primary_key("access_token");
72
65
73
66
74
# Created by DBIx::Class::Schema::Loader v0.07046 @ 2018-03-14 12:13:59
67
# Created by DBIx::Class::Schema::Loader v0.07046 @ 2018-04-11 17:44:30
75
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:VgSA5BIbeUR31WV1YwbfEQ
68
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:u2e++Jrwln4Qhi3UPx2CQA
76
69
77
70
78
# You can replace this text with custom code or comments, and it will be preserved on regeneration
71
# You can replace this text with custom code or comments, and it will be preserved on regeneration
(-)a/api/v1/swagger/paths/oauth.json (-6 lines)
Lines 26-37 Link Here
26
                    "in": "formData",
26
                    "in": "formData",
27
                    "description": "client secret",
27
                    "description": "client secret",
28
                    "type": "string"
28
                    "type": "string"
29
                },
30
                {
31
                    "name": "scope",
32
                    "in": "formData",
33
                    "description": "space-separated list of scopes",
34
                    "type": "string"
35
                }
29
                }
36
            ],
30
            ],
37
            "responses": {
31
            "responses": {
(-)a/api/v1/swagger/paths/patrons.json (-8 / +2 lines)
Lines 46-55 Link Here
46
        "permissions": {
46
        "permissions": {
47
          "borrowers": "edit_borrowers"
47
          "borrowers": "edit_borrowers"
48
        }
48
        }
49
      },
49
      }
50
      "x-koha-scopes": [
51
        "patrons.read"
52
      ]
53
    }
50
    }
54
  },
51
  },
55
  "/patrons/{borrowernumber}": {
52
  "/patrons/{borrowernumber}": {
Lines 108-117 Link Here
108
        "permissions": {
105
        "permissions": {
109
          "borrowers": "edit_borrowers"
106
          "borrowers": "edit_borrowers"
110
        }
107
        }
111
      },
108
      }
112
      "x-koha-scopes": [
113
        "patrons.read"
114
      ]
115
    }
109
    }
116
  }
110
  }
117
}
111
}
(-)a/etc/koha-conf.xml (+15 lines)
Lines 127-132 __PAZPAR2_TOGGLE_XML_POST__ Link Here
127
 <!-- Secret passphrase used by Mojolicious for signed cookies -->
127
 <!-- Secret passphrase used by Mojolicious for signed cookies -->
128
 <api_secret_passphrase>CHANGEME</api_secret_passphrase>
128
 <api_secret_passphrase>CHANGEME</api_secret_passphrase>
129
129
130
 <!-- Uncomment and modify the following to enable OAuth2 authentication for the
131
      REST API -->
132
 <!--
133
 <api_client>
134
    <client_id>client1</client_id>
135
    <client_secret>secret1</client_secret>
136
    <patron_id>1</patron_id>
137
 </api_client>
138
 <api_client>
139
    <client_id>client2</client_id>
140
    <client_secret>secret2</client_secret>
141
    <patron_id>2</patron_id>
142
 </api_client>
143
 -->
144
130
 <!-- true type font mapping accoding to type from $font_types in C4/Creators/Lib.pm -->
145
 <!-- true type font mapping accoding to type from $font_types in C4/Creators/Lib.pm -->
131
 <ttf>
146
 <ttf>
132
    <font type="TR" >__FONT_DIR__/DejaVuSerif.ttf</font>
147
    <font type="TR" >__FONT_DIR__/DejaVuSerif.ttf</font>
(-)a/installer/data/mysql/atomicupdate/oauth_tokens.perl (-1 lines)
Lines 5-11 if (CheckVersion($DBversion)) { Link Here
5
        CREATE TABLE oauth_access_tokens (
5
        CREATE TABLE oauth_access_tokens (
6
            access_token VARCHAR(255) NOT NULL,
6
            access_token VARCHAR(255) NOT NULL,
7
            client_id VARCHAR(255) NOT NULL,
7
            client_id VARCHAR(255) NOT NULL,
8
            scope TEXT,
9
            expires INT NOT NULL,
8
            expires INT NOT NULL,
10
            PRIMARY KEY (access_token)
9
            PRIMARY KEY (access_token)
11
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8
10
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8
(-)a/misc/cronjobs/cleanup_database.pl (+11 lines)
Lines 84-89 Usage: $0 [-h|--help] [--sessions] [--sessdays DAYS] [-v|--verbose] [--zebraqueu Link Here
84
   --temp-uploads     Delete temporary uploads.
84
   --temp-uploads     Delete temporary uploads.
85
   --temp-uploads-days DAYS Override the corresponding preference value.
85
   --temp-uploads-days DAYS Override the corresponding preference value.
86
   --uploads-missing FLAG Delete upload records for missing files when FLAG is true, count them otherwise
86
   --uploads-missing FLAG Delete upload records for missing files when FLAG is true, count them otherwise
87
   --oauth-tokens     Delete expired OAuth2 tokens
87
USAGE
88
USAGE
88
    exit $_[0];
89
    exit $_[0];
89
}
90
}
Lines 109-114 my $special_holidays_days; Link Here
109
my $temp_uploads;
110
my $temp_uploads;
110
my $temp_uploads_days;
111
my $temp_uploads_days;
111
my $uploads_missing;
112
my $uploads_missing;
113
my $oauth_tokens;
112
114
113
GetOptions(
115
GetOptions(
114
    'h|help'            => \$help,
116
    'h|help'            => \$help,
Lines 132-137 GetOptions( Link Here
132
    'temp-uploads'      => \$temp_uploads,
134
    'temp-uploads'      => \$temp_uploads,
133
    'temp-uploads-days:i' => \$temp_uploads_days,
135
    'temp-uploads-days:i' => \$temp_uploads_days,
134
    'uploads-missing:i' => \$uploads_missing,
136
    'uploads-missing:i' => \$uploads_missing,
137
    'oauth-tokens'      => \$oauth_tokens,
135
) || usage(1);
138
) || usage(1);
136
139
137
# Use default values
140
# Use default values
Lines 165-170 unless ( $sessions Link Here
165
    || $special_holidays_days
168
    || $special_holidays_days
166
    || $temp_uploads
169
    || $temp_uploads
167
    || defined $uploads_missing
170
    || defined $uploads_missing
171
    || $oauth_tokens
168
) {
172
) {
169
    print "You did not specify any cleanup work for the script to do.\n\n";
173
    print "You did not specify any cleanup work for the script to do.\n\n";
170
    usage(1);
174
    usage(1);
Lines 336-341 if( defined $uploads_missing ) { Link Here
336
    }
340
    }
337
}
341
}
338
342
343
if ($oauth_tokens) {
344
    require Koha::OAuthAccessTokens;
345
346
    my $count = int Koha::OAuthAccessTokens->search({ expires => { '<=', time } })->delete;
347
    say "Removed $count expired OAuth2 tokens";
348
}
349
339
exit(0);
350
exit(0);
340
351
341
sub RemoveOldSessions {
352
sub RemoveOldSessions {
(-)a/misc/cronjobs/delete_expired_oauth_tokens.pl (-22 lines)
Lines 1-22 Link Here
1
#!/usr/bin/env perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
6
# terms of the GNU General Public License as published by the Free Software
7
# Foundation; either version 3 of the License, or (at your option) any later
8
# version.
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18
use Modern::Perl;
19
20
use Koha::OAuthAccessTokens;
21
22
Koha::OAuthAccessTokens->search({ expires => { '<=', time } })->delete;
(-)a/t/db_dependent/api/v1/oauth.t (-5 / +26 lines)
Lines 23-37 use Test::Mojo; Link Here
23
use Koha::Database;
23
use Koha::Database;
24
24
25
use t::lib::Mocks;
25
use t::lib::Mocks;
26
use t::lib::TestBuilder;
26
27
27
my $t = Test::Mojo->new('Koha::REST::V1');
28
my $t = Test::Mojo->new('Koha::REST::V1');
28
my $schema  = Koha::Database->new->schema;
29
my $schema  = Koha::Database->new->schema;
30
my $builder = t::lib::TestBuilder->new();
29
31
30
subtest '/oauth/token tests' => sub {
32
subtest '/oauth/token tests' => sub {
31
    plan tests => 17;
33
    plan tests => 19;
32
34
33
    $schema->storage->txn_begin;
35
    $schema->storage->txn_begin;
34
36
37
    my $patron = $builder->build({
38
        source => 'Borrower',
39
        value  => {
40
            surname => 'Test OAuth',
41
            flags => 0,
42
        },
43
    });
44
35
    # Missing parameter grant_type
45
    # Missing parameter grant_type
36
    $t->post_ok('/api/v1/oauth/token')
46
    $t->post_ok('/api/v1/oauth/token')
37
        ->status_is(400);
47
        ->status_is(400);
Lines 50-63 subtest '/oauth/token tests' => sub { Link Here
50
    t::lib::Mocks::mock_config('api_client', {
60
    t::lib::Mocks::mock_config('api_client', {
51
        'client_id' => $client_id,
61
        'client_id' => $client_id,
52
        'client_secret' => $client_secret,
62
        'client_secret' => $client_secret,
53
        'scope' => ['patrons.read'],
63
        patron_id => $patron->{borrowernumber},
54
    });
64
    });
55
65
56
    my $formData = {
66
    my $formData = {
57
        grant_type => 'client_credentials',
67
        grant_type => 'client_credentials',
58
        client_id => $client_id,
68
        client_id => $client_id,
59
        client_secret => $client_secret,
69
        client_secret => $client_secret,
60
        scope => 'patrons.read',
61
    };
70
    };
62
    $t->post_ok('/api/v1/oauth/token', form => $formData)
71
    $t->post_ok('/api/v1/oauth/token', form => $formData)
63
        ->status_is(200)
72
        ->status_is(200)
Lines 70-78 subtest '/oauth/token tests' => sub { Link Here
70
    # Without access token, it returns 401
79
    # Without access token, it returns 401
71
    $t->get_ok('/api/v1/patrons')->status_is(401);
80
    $t->get_ok('/api/v1/patrons')->status_is(401);
72
81
73
    # With access token, it returns 200
82
    # With access token, but without permissions, it returns 403
74
    my $tx = $t->ua->build_tx(GET => '/api/v1/patrons');
83
    my $tx = $t->ua->build_tx(GET => '/api/v1/patrons');
75
    $tx->req->headers->authorization("Bearer $access_token");
84
    $tx->req->headers->authorization("Bearer $access_token");
85
    $t->request_ok($tx)->status_is(403);
86
87
    # With access token and permissions, it returns 200
88
    $builder->build({
89
        source => 'UserPermission',
90
        value  => {
91
            borrowernumber => $patron->{borrowernumber},
92
            module_bit => 4, # borrowers
93
            code => 'edit_borrowers',
94
        },
95
    });
96
    $tx = $t->ua->build_tx(GET => '/api/v1/patrons');
97
    $tx->req->headers->authorization("Bearer $access_token");
76
    $t->request_ok($tx)->status_is(200);
98
    $t->request_ok($tx)->status_is(200);
77
99
78
    $schema->storage->txn_rollback;
100
    $schema->storage->txn_rollback;
79
- 

Return to bug 20402