]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm
LP2042879 Shelving Location Groups Admin accessibility
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / AuthProxy.pm
1 #!/usr/bin/perl
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 =head1 NAME
18
19 OpenILS::Application::AuthProxy - Negotiator for proxy-style authentication
20
21 =head1 AUTHOR
22
23 Dan Wells, dbw2@calvin.edu
24
25 =cut
26
27 package OpenILS::Application::AuthProxy;
28
29 use strict;
30 use warnings;
31 use OpenILS::Application;
32 use base qw/OpenILS::Application/;
33 use OpenSRF::Utils::Cache;
34 use OpenSRF::Utils::Logger qw(:logger);
35 use OpenSRF::Utils::SettingsClient;
36 use OpenILS::Application::AppUtils;
37 use OpenILS::Utils::Fieldmapper;
38 use OpenILS::Utils::CStoreEditor qw/:funcs/;
39 use OpenILS::Event;
40 use UNIVERSAL::require;
41 use Digest::MD5 qw/md5_hex/;
42 my $U = 'OpenILS::Application::AppUtils';
43
44 # NOTE: code assumes throughout that '0' is never a valid username, barcode,
45 # or password; some logic will need to be tweaked to support it if needed.
46
47 my @authenticators;
48 my %authenticators_by_name;
49 my $enabled = 'false';
50 my $cache;
51 my $seed_timeout;
52 my $block_timeout;
53 my $block_count;
54
55 sub initialize {
56     my $conf = OpenSRF::Utils::SettingsClient->new;
57     $cache = OpenSRF::Utils::Cache->new();
58
59     my @pfx = ( "apps", "open-ils.auth", "app_settings", "auth_limits" );
60
61     # read in (or set defaults) for brute force blocking settings
62     $seed_timeout = $conf->config_value( @pfx, "seed" );
63     $seed_timeout = 30 if (!$seed_timeout or $seed_timeout < 0);
64     $block_timeout = $conf->config_value( @pfx, "block_time" );
65     $block_timeout = $seed_timeout * 3 if (!$block_timeout or $block_timeout < 0);
66     $block_count = $conf->config_value( @pfx, "block_count" );
67     $block_count = 10 if (!$block_count or $block_count < 0);
68
69     @pfx = ( "apps", "open-ils.auth_proxy", "app_settings" );
70
71     $enabled = $conf->config_value( @pfx, 'enabled' );
72
73     my $auth_configs = $conf->config_value( @pfx, 'authenticators', 'authenticator' );
74     $auth_configs = [$auth_configs] if ref($auth_configs) eq 'HASH';
75
76     if ( !@$auth_configs ) {
77         $logger->error("AuthProxy: authenticators list not found!");
78     } else {
79         foreach my $auth_config (@$auth_configs) {
80             my $auth_handler;
81             if ($auth_config->{'name'} eq 'native') {
82                 $auth_handler = 'OpenILS::Application::AuthProxy::Native';
83             } else {
84                 $auth_handler = $auth_config->{module};
85                 next unless $auth_handler;
86
87                 $logger->debug("Attempting to load AuthProxy handler: $auth_handler");
88                 $auth_handler->use;
89                 if($@) {
90                     $logger->error("Unable to load AuthProxy handler [$auth_handler]: $@");
91                     next;
92                 }
93             }
94
95             &_make_option_array($auth_config, 'login_types', 'type');
96             &_make_option_array($auth_config, 'org_units', 'unit');
97
98             my $authenticator = $auth_handler->new($auth_config);
99             push @authenticators, $authenticator;
100             $authenticators_by_name{$authenticator->name} = $authenticator;
101             $logger->debug("Successfully loaded AuthProxy handler: $auth_handler");
102         }
103         $logger->debug("AuthProxy: authenticators loaded");
104     }
105 }
106
107 # helper function to simplify the config structure
108 sub _make_option_array {
109     my ($auth_config, $container_name, $node_name) = @_;
110
111     if (exists $auth_config->{$container_name}
112         and ref $auth_config->{$container_name} eq 'HASH') {
113         my $nodes = $auth_config->{$container_name}{$node_name};
114         if ($nodes) {
115             if (ref $nodes ne 'ARRAY') {
116                 $auth_config->{$container_name} = [$nodes];
117             } else {
118                 $auth_config->{$container_name} = $nodes;
119             }
120         } else {
121             delete $auth_config->{$container_name};
122         }
123     } else {
124         delete $auth_config->{$container_name};
125     }
126 }
127
128
129
130 __PACKAGE__->register_method(
131     method    => "enabled",
132     api_name  => "open-ils.auth_proxy.enabled",
133     api_level => 1,
134     stream    => 1,
135     argc      => 0,
136     signature => {
137         desc => q/Check if AuthProxy is enabled/,
138         return => {
139             desc => "True if enabled, false if not",
140             type => "bool"
141         }
142     }
143 );
144 sub enabled {
145     return (!$enabled or $enabled eq 'false') ? 0 : 1;
146 }
147
148 __PACKAGE__->register_method(
149     method    => "login",
150     api_name  => "open-ils.auth_proxy.login",
151     api_level => 1,
152     stream    => 1,
153     argc      => 1,
154     signature => {
155         desc => q/Basic single-factor login method/,
156         params => [
157             {name=> "args", desc => q/A hash of arguments.  Valid keys and their meanings:
158     username := Username to authenticate.
159     barcode  := Barcode of user to authenticate 
160     password := Password for verifying the user.
161     type     := Type of login being attempted (Staff Client, OPAC, etc.).
162     org      := Org unit id
163 /,
164                 type => "hash"}
165         ],
166         return => {
167             desc => "Authentication seed or failure event",
168             type => "mixed"
169         }
170     }
171 );
172 sub login {
173     my ( $self, $conn, $args ) = @_;
174     $args ||= {};
175
176     return OpenILS::Event->new( 'LOGIN_FAILED' )
177       unless (&enabled() and ($args->{'username'} or $args->{'barcode'}));
178
179     # provided username may not be the user's actual EG username;
180     # hang onto the provided value (if any) so we can use it later
181     $args->{'provided_username'} = $args->{'username'};
182
183     if ($args->{barcode} and !$args->{username}) {
184         # translate barcode logins into username logins by locating
185         # the matching card/user and collecting the username.
186
187         my $card = new_editor()->search_actor_card([
188             {barcode => $args->{barcode}, active => 't'},
189             {flesh => 1, flesh_fields => {ac => ['usr']}}
190         ])->[0];
191
192         if ($card) {
193             $args->{username} = $card->usr->usrname;
194         } else { # must have or resolve to a username
195             return OpenILS::Event->new( 'LOGIN_FAILED' );
196         }
197     }
198
199     # check for possibility of brute-force attack
200     my $fail_count = $cache->get_cache('oils_auth_' . $args->{'username'} . '_count') || 0;
201     if ($fail_count >= $block_count) {
202         $logger->debug("AuthProxy found too many recent failures for '" . $args->{'username'} . "' : $fail_count, forcing failure state.");
203         $cache->put_cache('oils_auth_' . $args->{'username'} . '_count', ++$fail_count, $block_timeout);
204         return OpenILS::Event->new( 'LOGIN_FAILED' );
205     }
206
207     my @error_events;
208     my $authenticated = 0;
209     my $auths;
210
211     # if they specify an authenticator by name, only try that one
212     if ($args->{'name'}) {
213         $auths = [$authenticators_by_name{$args->{'name'}}];
214     } else {
215         $auths = \@authenticators;
216     }
217
218     foreach my $authenticator (@$auths) {
219         # skip authenticators specified for a different login type
220         # or org unit id
221         if ($authenticator->login_types and $args->{'type'}) {
222             next unless grep(/^(all|$args->{'type'})$/, @{$authenticator->{'login_types'}});
223         }
224         if ($authenticator->org_units and $args->{'org'}) {
225             next unless grep(/^(all|$args->{'org'})$/, @{$authenticator->{'org_units'}});
226         }
227
228         my $event;
229         # treat native specially
230         if ($authenticator->name eq 'native') {
231             $event = &_do_login($args);
232         } else {
233             $event = $authenticator->authenticate($args);
234         }
235         my $code = $U->event_code($event);
236         if ($code) {
237             push @error_events, $event;
238         } elsif (defined $code) { # code is '0', i.e. SUCCESS
239             if ($authenticator->name eq 'native' and exists $event->{'payload'}) { # we have a complete native login
240                 return $event;
241             } else { # create an EG session for the successful external login
242                 # if external login returns a payload, that payload is the
243                 # user's Evergreen username
244                 if ($event->{'payload'}) {
245                     $args->{'username'} = $event->{'payload'};
246                 }
247
248                 # before we actually create the session, let's first check if
249                 # Evergreen thinks this user is allowed to login
250                 #
251                 # (we do this *after* authentication to avoid any personal data
252                 # leakage)
253
254                 # get the user id
255                 my $user = $U->cstorereq(
256                     "open-ils.cstore.direct.actor.user.search.atomic",
257                     { usrname => $args->{'username'} }
258                 );
259                 if (!$user->[0]) {
260                     $logger->debug("Authenticated username '" . $args->{'username'} . "' has no Evergreen account, aborting");
261                     return OpenILS::Event->new( 'LOGIN_FAILED' );
262                 } else {
263                     my $restrict_by_ou = $authenticator->{restrict_by_home_ou};
264                     if (defined($restrict_by_ou) and $restrict_by_ou =~ /^t/i) {
265                         my $home_ou = $user->[0]->home_ou;
266                         my $allowed = 0;
267                         # disallow auth if user's home library is not one of the org_units for this authenticator
268                         if ($authenticator->org_units) {
269                             if (grep(/^all$/, @{$authenticator->org_units})) {
270                                 $allowed = 1;
271                             } else {
272                                 foreach my $org (@{$authenticator->org_units}) {
273                                     my $allowed_orgs = $U->get_org_descendants($org);
274                                     if (grep(/^$home_ou$/, @$allowed_orgs)) {
275                                         $allowed = 1;
276                                         last;
277                                     }
278                                 }
279                             }
280                             if (!$allowed) {
281                                 $logger->debug("Auth disallowed for matching user's home library, aborting");
282                                 return OpenILS::Event->new( 'LOGIN_FAILED' );
283                             }
284                         }
285                     }
286                     $args->{user_id} = $user->[0]->id;
287                 }
288
289                 # validate the account
290                 my $trimmed_args = {
291                     user_id => $args->{user_id},
292                     login_type => $args->{type},
293                     workstation => $args->{workstation},
294                     org_unit => $args->{org}
295                 };
296                 $event = &_auth_internal('user.validate', $trimmed_args);
297                 if ($U->event_code($event)) { # non-zero = we didn't succeed
298                     # can't recover from invalid user, return right away
299                     return $event;
300                 } else { # it's all good
301                     return &_auth_internal('session.create', $trimmed_args);
302                 }
303             }
304         }
305     }
306
307     # if we got this far, we failed
308     # increment the brute force counter if 'native' didn't already
309     if (!exists $authenticators_by_name{'native'}) {
310         $cache->put_cache('oils_auth_' . $args->{'username'} . '_count', ++$fail_count, $block_timeout);
311     }
312     # TODO: send back some form of collected error events
313     return OpenILS::Event->new( 'LOGIN_FAILED' );
314 }
315
316 sub _auth_internal {
317     my ($method, $args) = @_;
318
319     my $response = OpenSRF::AppSession->create("open-ils.auth_internal")->request(
320         'open-ils.auth_internal.'.$method,
321         $args
322     )->gather(1);
323
324     return OpenILS::Event->new( 'LOGIN_FAILED' )
325       unless $response;
326
327     return $response;
328 }
329
330 sub _do_login {
331     my $args = shift;
332     my $authenticated = shift;
333
334     my $seeder = $args->{'username'} ? $args->{'username'} : $args->{'barcode'};
335     my $seed =
336       OpenSRF::AppSession->create("open-ils.auth")
337       ->request( 'open-ils.auth.authenticate.init', $seeder )->gather(1);
338
339     return OpenILS::Event->new( 'LOGIN_FAILED' )
340       unless $seed;
341
342     my $real_password = $args->{'password'};
343     $args->{'password'} = md5_hex( $seed . md5_hex($real_password) );
344     my $response = OpenSRF::AppSession->create("open-ils.auth")->request(
345         'open-ils.auth.authenticate.complete',
346         $args
347     )->gather(1);
348     $args->{'password'} = $real_password;
349
350     return OpenILS::Event->new( 'LOGIN_FAILED' )
351       unless $response;
352
353     return $response;
354 }
355
356 __PACKAGE__->register_method(
357     method    => "authenticators",
358     api_name  => "open-ils.auth_proxy.authenticators",
359     api_level => 1,
360     stream    => 1,
361     argc      => 1,
362     signature => {
363         desc => q/Get a list of viable authenticators/,
364         params => [
365             {name=> "args", desc => q/A hash of arguments.  Valid keys and their meanings:
366     type     := Type of login being attempted (Staff Client, OPAC, etc.).
367     org      := Org unit id
368 /,
369                 type => "hash"}
370         ],
371         return => {
372             desc => "List of viable authenticators",
373             type => "array"
374         }
375     }
376 );
377 sub authenticators {
378     my ( $self, $conn, $args ) = @_;
379
380     my @viable_auths;
381
382     foreach my $authenticator (@authenticators) {
383         # skip authenticators specified for a different login type
384         # or org unit id
385         if ($authenticator->login_types and $args->{'type'}) {
386             next unless grep(/^(all|$args->{'type'})$/, @{$authenticator->login_types});
387         }
388         if ($authenticator->org_units and $args->{'org'}) {
389             next unless grep(/^(all|$args->{'org'})$/, @{$authenticator->org_units});
390         }
391
392         push @viable_auths, $authenticator->name;
393     }
394
395     return \@viable_auths;
396 }
397
398
399 # --------------------------------------------------------------------------
400 # Stub package for 'native' authenticator
401 # --------------------------------------------------------------------------
402 package OpenILS::Application::AuthProxy::Native;
403 use strict; use warnings;
404 use base 'OpenILS::Application::AuthProxy::AuthBase';
405
406 1;