1 # Copyright (C) 2019 BC Libraries Cooperative
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.
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.
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.
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 # ======================================================================
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;
28 use OpenSRF::EX qw(:try);
29 use OpenSRF::Utils::Logger qw/$logger/;
31 use OpenILS::Utils::CStoreEditor qw/:funcs/;
32 use OpenILS::Application::AppUtils;
33 our $U = "OpenILS::Application::AppUtils";
36 my @handlers_to_preinit = ();
39 my ($self, $editor) = @_;
40 $self->{editor} = $editor if $editor;
41 return $self->{editor};
45 my ($self, $config) = @_;
46 $self->{config} = $config if $config;
47 return $self->{config};
51 my ($self, $bootstrap_config, $handlers) = @_;
52 @handlers_to_preinit = split /\s+/, $handlers, -1 if defined($handlers);
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) {
65 return Apache2::Const::OK;
71 my $stat = Apache2::Const::AUTH_REQUIRED;
73 # load the appropriate module and process our request
75 my $module = $r->dir_config('OILSRemoteAuthHandler');
77 my $handler = $module->new;
78 $stat = $handler->process($r);
81 $logger->error("processing RemoteAuth handler failed: $err");
82 $stat = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
89 my ($self, $e, $r) = @_;
91 # name to use for config lookup
92 my $name = $r->dir_config('OILSRemoteAuthProfile');
93 return undef unless $name;
96 my $config = $e->retrieve_config_remoteauth_profile($name);
97 if ($config and $U->is_true($config->enabled)) {
100 $logger->info("RemoteAuth: config profile $name not found (or not enabled)");
105 my ($self, $client_username, $client_password) = @_;
106 my $login_resp = $U->simplereq(
108 'open-ils.auth.login', {
109 username => $client_username,
110 password => $client_password,
114 if ($login_resp->{textcode} eq 'SUCCESS') {
115 return $login_resp->{payload}->{authtoken};
117 $logger->info("RemoteAuth: failed to authenticate client $client_username");
122 my ($self, $e, $config, $id, $password) = @_;
123 my $org_unit = $config->context_org;
125 return $self->backend_error unless $e->checkauth;
128 type => 'opac', # XXX
131 password => $password
134 my $cuat = $e->retrieve_config_usr_activity_type($config->usr_activity_type);
136 $args->{agent} = $cuat->ewho;
139 my $response = $U->simplereq(
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;
147 # get basic patron info via user authtoken
148 my $authtoken = $response->{payload}->{authtoken};
149 my $user = $U->simplereq(
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;
156 my $userid = $user->id;
157 my $home_ou = $user->home_ou;
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;
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;
170 if ($U->is_true($user->barred)) {
171 $logger->info("RemoteAuth: user $userid is barred");
172 return $self->patron_is_blocked;
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'};;
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;
187 return $self->patron_is_blocked;
191 # NB: This method returns patron info without patron authorization!
193 sub get_patron_info {
194 my ($self, $e, $config, $args) = @_;
195 my $authtoken = $e->authtoken;
196 my $org_unit = $config->context_org;
199 return $self->backend_error unless $e->checkauth;
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(
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;
217 if (scalar @$result == 0) {
218 return $self->patron_not_found;
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;
225 $userid = $result->[0]->{id};
227 $logger->error('RemoteAuth: get_patron_info: patron id not provided (or invalid id type)');
228 return $self->backend_error;
231 $logger->error("RemoteAuth get_patron_info failed to retrieve userid: @_");
232 return $self->backend_error;
235 return $self->patron_not_found unless ($userid);
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'};
242 if ($permit_test eq 'success') {
243 # permit_test succeeded, retrieve fleshed user info
250 "standing_penalties",
253 ausp => [ "standing_penalty" ]
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;
261 return $self->success($user);
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;
271 return $self->backend_error;
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.
280 # patron auth succeeded
282 return Apache2::Const::OK;
285 # generic backend error
287 return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
290 # client error (e.g. missing params)
292 return Apache2::Const::HTTP_BAD_REQUEST;
296 sub client_not_authorized {
297 return Apache2::Const::AUTH_REQUIRED;
300 # patron auth failed (bad password etc)
301 sub patron_not_authenticated {
302 return Apache2::Const::FORBIDDEN;
305 # patron does not exist or is inactive/deleted
306 sub patron_not_found {
307 return Apache2::Const::DECLINED;
310 # patron is barred or has blocking penalties
311 sub patron_is_blocked {
312 return Apache2::Const::FORBIDDEN;
316 sub patron_is_expired {
317 return Apache2::Const::DECLINED;