]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth.pm
LP#1887196: RemoteAuth: add method to retrieve detailed patron info
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / WWW / RemoteAuth.pm
1 # Copyright (C) 2019 BC Libraries Cooperative
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16
17 # ====================================================================== 
18 # - base class for configurable HTTP API for patron auth/retrieval
19 # - provides generic methods shared by all handler subclasses
20 # - handlers take care of endpoint-specific implementation details
21 # ======================================================================
22
23 package OpenILS::WWW::RemoteAuth;
24 use strict; use warnings;
25 use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN AUTH_REQUIRED HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
26 use DateTime::Format::ISO8601;
27
28 use OpenSRF::EX qw(:try);
29 use OpenSRF::Utils::Logger qw/$logger/;
30 use OpenSRF::System;
31 use OpenILS::Utils::CStoreEditor qw/:funcs/;
32 use OpenILS::Application::AppUtils;
33 our $U = "OpenILS::Application::AppUtils";
34
35 my $bootstrap_config;
36 my @handlers_to_preinit = ();
37
38 sub editor {
39     my ($self, $editor) = @_;
40     $self->{editor} = $editor if $editor;
41     return $self->{editor};
42 }
43
44 sub config {
45     my ($self, $config) = @_;
46     $self->{config} = $config if $config;
47     return $self->{config};
48 }
49
50 sub import {
51     my ($self, $bootstrap_config, $handlers) = @_;
52     @handlers_to_preinit = split /\s+/, $handlers, -1 if defined($handlers);
53 }
54
55 sub child_init {
56     OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
57     my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
58     Fieldmapper->import(IDL => $idl);
59     OpenILS::Utils::CStoreEditor->init;
60     foreach my $module (@handlers_to_preinit) {
61         eval {
62             $module->use;
63         };
64     }
65     return Apache2::Const::OK;
66 }
67
68 sub handler {
69     my $r = shift;
70
71     my $stat = Apache2::Const::AUTH_REQUIRED;
72
73     # load the appropriate module and process our request
74     try {
75         my $module = $r->dir_config('OILSRemoteAuthHandler');
76         $module->use;
77         my $handler = $module->new;
78         $stat = $handler->process($r);
79     } catch Error with {
80         my $err = shift;
81         $logger->error("processing RemoteAuth handler failed: $err");
82         $stat = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
83     };
84
85     return $stat;
86 }
87
88 sub load_config {
89     my ($self, $e, $r) = @_;
90
91     # name to use for config lookup
92     my $name = $r->dir_config('OILSRemoteAuthProfile');
93     return undef unless $name;
94
95     # load config
96     my $config = $e->retrieve_config_remoteauth_profile($name);
97     if ($config and $U->is_true($config->enabled)) {
98         return $config;
99     }
100     $logger->info("RemoteAuth: config profile $name not found (or not enabled)");
101     return undef;
102 }
103
104 sub do_client_auth {
105     my ($self, $client_username, $client_password) = @_;
106     my $login_resp = $U->simplereq(
107         'open-ils.auth',
108         'open-ils.auth.login', {
109             username => $client_username,
110             password => $client_password,
111             type => 'staff'
112         }   
113     );
114     if ($login_resp->{textcode} eq 'SUCCESS') {
115         return $login_resp->{payload}->{authtoken};
116     }
117     $logger->info("RemoteAuth: failed to authenticate client $client_username");
118     return undef;
119 }
120
121 sub do_patron_auth {
122     my ($self, $e, $config, $id, $password) = @_;
123     my $org_unit = $config->context_org;
124
125     return $self->backend_error unless $e->checkauth;
126
127     my $args = {
128         type => 'opac', # XXX
129         org => $org_unit,
130         identifier => $id,
131         password => $password
132     };
133
134     my $cuat = $e->retrieve_config_usr_activity_type($config->usr_activity_type);
135     if ($cuat) {
136         $args->{agent} = $cuat->ewho;
137     }
138
139     my $response = $U->simplereq(
140         'open-ils.auth',
141         'open-ils.auth.login', $args);
142     if($U->event_code($response)) { 
143         $logger->info("RemoteAuth: failed to authenticate user $id at org unit $org_unit");
144         return $self->patron_not_authenticated;
145     }
146
147     # get basic patron info via user authtoken
148     my $authtoken = $response->{payload}->{authtoken};
149     my $user = $U->simplereq(
150         'open-ils.auth',
151         'open-ils.auth.session.retrieve', $authtoken);
152     if (!$user or $U->event_code($user)) {
153         $logger->error("RemoteAuth: failed to retrieve user for session $authtoken");
154         return $self->backend_error;
155     }
156     my $userid = $user->id;
157     my $home_ou = $user->home_ou;
158
159     unless ($e->allowed('VIEW_USER', $home_ou)) {
160         $logger->info("RemoteAuth: client does not have permission to view user $userid");
161         return $self->client_not_authorized;
162     }
163
164     # do basic validation (and skip the permit test where applicable)
165     if ($U->is_true($user->deleted)) {
166         $logger->info("RemoteAuth: user $userid is deleted");
167         return $self->patron_not_found;
168     }
169
170     if ($U->is_true($user->barred)) {
171         $logger->info("RemoteAuth: user $userid is barred");
172         return $self->patron_is_blocked;
173     }
174
175     # check if remoteauth is permitted for this user
176     my $permit_test = $e->json_query(
177         {from => ['actor.permit_remoteauth', $config->name, $userid]}
178     )->[0]{'actor.permit_remoteauth'};;
179
180     if ($permit_test eq 'success') {
181         return $self->success($user);
182     } elsif ($permit_test eq 'not_found') {
183         return $self->patron_not_found;
184     } elsif ($permit_test eq 'expired') {
185         return $self->patron_is_expired;
186     } else {
187         return $self->patron_is_blocked;
188     }
189 }
190
191 # NB: This method returns patron info without patron authorization!
192 # Use with caution.
193 sub get_patron_info {
194     my ($self, $e, $config, $args) = @_;
195     my $authtoken = $e->authtoken;
196     my $org_unit = $config->context_org;
197     my $userid;
198
199     return $self->backend_error unless $e->checkauth;
200
201     try {
202         if ($args->{userid}) {
203             $userid = $args->{userid};
204         } elsif ($args->{username}) {
205             my $result = $e->search_actor_user({ usrname => $args->{username} })->[0]
206                 or return $self->patron_not_found;
207             $userid = $result->id;
208         } elsif ($args->{barcode}) {
209             my $result = $U->simplereq(
210                 'open-ils.actor',
211                 'open-ils.actor.get_barcodes',
212                 $authtoken, $org_unit, 'actor', $args->{barcode});
213             if (!$result or $U->event_code($result)) {
214                 $logger->error("RemoteAuth: failed to retrieve user for session $authtoken");
215                 return $self->backend_error;
216             }
217             if (scalar @$result == 0) {
218                 return $self->patron_not_found;
219             }
220             if (scalar @$result > 1) {
221                 # TODO handle multiple matching barcodes
222                 $logger->error("RemoteAuth: too many matching patrons at org unit $org_unit for barcode " . $args->{barcode});
223                 return $self->backend_error;
224             }
225             $userid = $result->[0]->{id};
226         } else {
227             $logger->error('RemoteAuth: get_patron_info: patron id not provided (or invalid id type)');
228             return $self->backend_error;
229         }
230     } catch Error with {
231         $logger->error("RemoteAuth get_patron_info failed to retrieve userid: @_");
232         return $self->backend_error;
233     };
234
235     return $self->patron_not_found unless ($userid);
236
237     # check if remoteauth is permitted for this user
238     my $permit_test = $e->json_query(
239         {from => ['actor.permit_remoteauth', $config->name, $userid]}
240     )->[0]{'actor.permit_remoteauth'};
241
242     if ($permit_test eq 'success') {
243         # permit_test succeeded, retrieve fleshed user info
244         my $usr_flesh = {
245             flesh => 2,
246             flesh_fields => {
247                 au => [
248                     "card",
249                     "cards",
250                     "standing_penalties",
251                     'profile'
252                 ],
253                 ausp => [ "standing_penalty" ]
254             }
255         };
256         my $user = $e->retrieve_actor_user([$userid, $usr_flesh]);
257         if (!$user or $U->event_code($user)) {
258             $logger->error("RemoteAuth: failed to retrieve user for session $authtoken");
259             return $self->backend_error;
260         } else {
261             return $self->success($user);
262         }
263
264     } elsif ($permit_test eq 'not_found') {
265         return $self->patron_not_found;
266     } elsif ($permit_test eq 'expired') {
267         return $self->patron_is_expired;
268     } elsif ($permit_test eq 'blocked') {
269         return $self->patron_is_blocked;
270     } else {
271         return $self->backend_error;
272     }
273 }
274
275 # Dummy methods for responding to the client based on
276 # different error (or success) conditions.
277 # The handler will normally want to override these methods
278 # with its own version of them.
279
280 # patron auth succeeded
281 sub success {
282     return Apache2::Const::OK;
283 }
284
285 # generic backend error
286 sub backend_error {
287     return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
288 }
289
290 # client error (e.g. missing params)
291 sub client_error {
292     return Apache2::Const::HTTP_BAD_REQUEST;
293 }
294
295 # client auth failed
296 sub client_not_authorized {
297     return Apache2::Const::AUTH_REQUIRED;
298 }
299
300 # patron auth failed (bad password etc)
301 sub patron_not_authenticated {
302     return Apache2::Const::FORBIDDEN;
303 }
304
305 # patron does not exist or is inactive/deleted
306 sub patron_not_found {
307     return Apache2::Const::DECLINED;
308 }
309
310 # patron is barred or has blocking penalties
311 sub patron_is_blocked {
312     return Apache2::Const::FORBIDDEN;
313 }
314
315 # patron is expired
316 sub patron_is_expired {
317     return Apache2::Const::DECLINED;
318 }
319
320 1;
321