File Coverage

blib/lib/PlugAuth.pm
Criterion Covered Total %
statement 98 108 90.7
branch 27 36 75.0
condition 7 9 77.7
subroutine 19 20 95.0
pod 2 2 100.0
total 153 175 87.4


line stmt bran cond sub pod time code
1             package PlugAuth;
2              
3             # ABSTRACT: Pluggable authentication and authorization server.
4             our $VERSION = '0.35'; # VERSION
5              
6              
7 41     41   3231937 use strict;
  41         80  
  41         1429  
8 41     41   210 use warnings;
  41         71  
  41         1273  
9 41     41   1063 use 5.010001;
  41         167  
10 41     41   222 use base 'Clustericious::App';
  41         169  
  41         8603  
11 41     41   7443980 use PlugAuth::Routes;
  41         133  
  41         1812  
12 41     41   331 use Log::Log4perl qw( :easy );
  41         70  
  41         269  
13 41     41   26803 use Role::Tiny ();
  41         143157  
  41         1075  
14 41     41   7685 use PlugAuth::Role::Plugin;
  41         104  
  41         1471  
15 41     41   302 use Clustericious::Config;
  41         66  
  41         1055  
16 41     41   197 use Mojo::Base 'Mojo::EventEmitter';
  41         69  
  41         393  
17              
18             sub plugin
19             {
20 430     430 1 1190788 my($self, $name, @rest) = @_;
21            
22             # catch load of plug_auth and use self_plug_auth instead
23 430 100 66     3459 if($name eq 'plug_auth'
24             || $name eq 'PlugAuth')
25             {
26 25         208 TRACE "loading plugin self_plug_auth ($name)";
27 25         13168 return $self->SUPER::plugin('self_plug_auth', @rest);
28             }
29              
30             # load the plugin
31 405         2060 TRACE "loading plugin $name";
32 405         98083 my $ret = $self->SUPER::plugin($name, @rest);
33            
34             # just in case we miss it and plug_auth was loaded,
35             # load self_plug_auth instead.
36 405 50       1147740 if(eval { $ret->isa('Clustericious::Plugin::PlugAuth') })
  405         5004  
37             {
38 0         0 TRACE "detected plug_auth plugin, switching with self_plug_auth";
39 0         0 return $self->SUPER::plugin('self_plug_auth', @rest);
40             }
41             else
42             {
43 405         1636 return $ret;
44             }
45             }
46              
47             sub startup
48             {
49 45     45 1 2113 my $self = shift;
50 45         566 $self->SUPER::startup(@_);
51              
52             #$self->renderer->default_format('txt');
53            
54 45         426138 my @plugins_config = eval {
55 45   100     291 my $plugins = $self->config->{plugins} // [];
56 45 100       1061 ref($plugins) eq 'ARRAY' ? @$plugins : ($plugins);
57             };
58              
59 45         286 my $auth_plugin;
60             my $authz_plugin;
61 0         0 my $welcome_plugin;
62 0         0 my @refresh;
63            
64 45         170 foreach my $plugin_class (reverse @plugins_config)
65             {
66 21         165 my $plugin_config;
67 21 100       88 if(ref $plugin_class)
68             {
69 13         73 ($plugin_config) = values %$plugin_class;
70 13 50       162 $plugin_config = Clustericious::Config->new($plugin_config)
71             unless ref($plugin_config) eq 'ARRAY';
72 13         7684 ($plugin_class) = keys %$plugin_class;
73             }
74             else
75             {
76 8         46 $plugin_config = Clustericious::Config->new({});
77             }
78            
79 21         5930 eval qq{ require $plugin_class };
80 21 50       208 LOGDIE $@ if $@;
81 21 50       99 Role::Tiny::does_role($plugin_class, 'PlugAuth::Role::Plugin')
82             || LOGDIE "$plugin_class is not a PlugAuth plugin";
83            
84 21         565 my $plugin = $plugin_class->new($self->config, $plugin_config, $self);
85              
86 21 100       92 if($plugin->does('PlugAuth::Role::Auth'))
87             {
88 17         317 $plugin->next_auth($auth_plugin);
89 17         36 $auth_plugin = $plugin;
90             }
91            
92 21 50       129 if($plugin->does('PlugAuth::Role::Welcome'))
93             {
94 0         0 $welcome_plugin = $plugin;
95             }
96              
97 21 100       328 $authz_plugin = $plugin if $plugin->does('PlugAuth::Role::Authz');
98 21 100       273 push @refresh, $plugin if $plugin->does('PlugAuth::Role::Refresh')
99             }
100              
101 45 100       429 unless(defined $auth_plugin)
102             {
103 33         3390 require PlugAuth::Plugin::FlatAuth;
104 33 100       428 if($self->config->ldap(default => ''))
105             {
106 1         591 require PlugAuth::Plugin::LDAP;
107 1         8 $auth_plugin = PlugAuth::Plugin::LDAP->new($self->config, {}, $self);
108 1         4 $auth_plugin->next_auth(PlugAuth::Plugin::FlatAuth->new($self->config, Clustericious::Config->new({}), $self));
109 1         5 push @refresh, $auth_plugin->next_auth;
110             }
111             else
112             {
113 32         2467 $auth_plugin = PlugAuth::Plugin::FlatAuth->new($self->config, Clustericious::Config->new({}), $self);
114 32         132 push @refresh, $auth_plugin;
115             }
116             }
117            
118 45 100       246 unless(defined $authz_plugin)
119             {
120 41         4698 require PlugAuth::Plugin::FlatAuthz;
121 41         593 $authz_plugin = PlugAuth::Plugin::FlatAuthz->new($self->config, Clustericious::Config->new({}), $self);
122 41         182 push @refresh, $authz_plugin;
123             }
124              
125 45     4   759 $self->helper(data => sub { $auth_plugin });
  4         3382  
126 45     375   1755 $self->helper(auth => sub { $auth_plugin });
  375         87607  
127 45     674   797 $self->helper(authz => sub { $authz_plugin });
  674         54591  
128            
129 45 50       796 if(@refresh > 0 )
130             {
131             $self->hook(before_dispatch => sub {
132 578     578   5028347 $_->refresh for @refresh;
133 45         491 });
134 45     83   1069 $self->helper(refresh => sub { $_->refresh for @refresh; 1 });
  83         6599  
  83         335  
135             }
136             else
137             {
138 0     0   0 $self->helper(refresh => sub { 1; });
  0         0  
139             }
140            
141             $self->helper(welcome => sub {
142 2     2   106 my($self) = @_;
143 2 50       10 if($welcome_plugin)
144             {
145 0         0 $welcome_plugin->welcome($self);
146             }
147             else
148             {
149 2         29 $self->render_message("welcome to plug auth");
150             }
151 45         864 });
152            
153             # for historical reasons, some routes return text by default
154             # in older versions, even if a format (e.g. /auth.json) is specified.
155             # this is a simple helper to render using autodata if a format
156             # is explicitly specified, otherwise fallback on the original
157             # behavior.
158             $self->helper(render_message => sub {
159 429     429   18012 my($self, $message, $status) = @_;
160 429   100     1878 $status //= 200;
161 429   50     1678 my $format = $self->stash->{format} // 'txt';
162 429 50       6881 if($format ne 'txt')
163             {
164 0         0 $self->render(autodata => { message => $message, status => $status }, status => $status);
165             }
166             else
167             {
168 429         2490 $self->render(text => $message, status => $status, format => 'txt');
169             }
170 45         839 });
171            
172             # Silence warnings; this is only used for for session
173             # cookies, which we don't use.
174 45 50       1074 if($self->can('secrets'))
175 45         329 { $self->secrets([rand]) }
176             else
177 0           { $self->secret(rand) }
178             }
179              
180             1;
181              
182             __END__
183              
184             =pod
185              
186             =encoding UTF-8
187              
188             =head1 NAME
189              
190             PlugAuth - Pluggable authentication and authorization server.
191              
192             =head1 VERSION
193              
194             version 0.35
195              
196             =head1 SYNOPSIS
197              
198             In the configuration file for the Clustericious app that will authenticate
199             against PlugAuth:
200              
201             ---
202             plug_auth:
203             url: http://localhost:1234
204              
205             and I<authenticate> and I<authorize> in your Clustericious application's Routes.pm:
206              
207             authenticate;
208             authorize;
209            
210             get '/resource' => sub {
211             # resource that requires authentication
212             # and authorization
213             };
214              
215             =head1 DESCRIPTION
216              
217             (For a quick start guide on how to setup a PlugAuth server, please see
218             L<PlugAuth::Guide::Server>)
219              
220             PlugAuth is a pluggable authentication and authorization server with a consistent
221             RESTful API. This allows clients to authenticate and query authorization from a
222             PlugAuth server without worrying or caring whether the actual authentication happens
223             against flat files, PAM, LDAP or passed on to another PlugAuth server.
224              
225             The authentication API is HTTP Basic Authentication. The authorization API is based
226             on users, groups, resources and hosts.
227              
228             The implementation for these can be swapped in and out depending on the plugins that
229             you select in the configuration file. The default plugins for authentication
230             (L<PlugAuth::Plugin::FlatAuth>) and authorization (L<PlugAuth::Plugin::FlatAuthz>) are
231             implemented with ordinary flat files and advisory locks using flock.
232              
233             The are other plugins for ldap (L<PlugAuth::Plugin::LDAP>), L<DBI>
234             (L<PlugAuth::Plugin::DBIAuth>), or you can write your own (L<PlugAuth::Guide::Plugin>).
235              
236             Here is a diagram that illustrates the most common use case for PlugAuth being used
237             by a RESTful service:
238              
239             client
240             |
241             | HTTP
242             |
243             +-----------+ +------------+ +--------------+
244             | REST | HTTP | | --> | Auth Plugin | --> files
245             | service | ------> | PlugAuth | +--------------+ --> ldap
246             | | | | --> | Authz Plugin | --> ...
247             +-----------+ +------------+ +--------------+
248              
249             =over 4
250              
251             =item 1.
252              
253             Client (web browser or other) sends an HTTP request to the service.
254              
255             =item 2
256              
257             The service sends an HTTP basic authentication request to PlugAuth with the user's credentials
258              
259             =item 3
260              
261             PlugAuth performs authentication (see L</AUTHENTICATION>) and returns the appropriate
262             HTTP status code.
263              
264             =item 4
265              
266             The REST service sends the HTTP status code to the client if authentication has failed.
267              
268             =item 5
269              
270             The REST service may optionally check the client's host, and if it is "trusted",
271             authorization succeeds (see L</AUTHORIZATION>).
272              
273             =item 6
274              
275             If not, the REST service sends an authorization request to PlugAuth, asking whether
276             the client has permission to perform an "action" on a "resource". Both the action and
277             resource are arbitrary strings, though one reasonable default is sending the HTTP
278             method as the action, and the URL path as the resource. (see L</AUTHORIZATION> below).
279              
280             =item 7
281              
282             PlugAuth returns a response code to the REST service indicating whether or not
283             authorization should succeed.
284              
285             =item 8
286              
287             The REST service returns the appropriate response to the client.
288              
289             =back
290              
291             If the REST service is written in Perl, see L<PlugAuth::Client>.
292              
293             If the REST service uses Clustericious, see L<Clustericious::Plugin::PlugAuth>.
294              
295             PlugAuth was originally written for scientific data processing clusters based on
296             L<Clustericious> in which all the services are RESTful servers distributed over a number
297             of different physical hosts, though it may be applicable in other contexts.
298              
299             =head2 AUTHENTICATION
300              
301             Checking for authentication is done by sending a GET request to URLs of the form
302              
303             /auth
304              
305             With the username and password specified as HTTP Basic credentials. The actual
306             mechanism used to verify authentication will depend on the authentication plugin being
307             used. The default is L<PlugAuth::Plugin::FlatAuth>.
308              
309             =head2 AUTHORIZATION
310              
311             Checking the authorization is done by sending GET requests to URLs of the form
312              
313             /authz/user/user/action/resource
314              
315             where I<user> and I<action> are strings (no slashes), and I<resource> is a string
316             which may have slashes. A response code of 200 indicates that access should be
317             granted, 403 indicates that the resource is forbidden. A user is granted access to a
318             resource if one of of the following conditions are met:
319              
320             =over 4
321              
322             =item
323              
324             the user is specifically granted access to that resource, i.e. a line of the form
325              
326             /resource (action): username
327              
328             appears in the resources file (see L</CONFIGURATION>).
329              
330             =item
331              
332             the user is a member of a group which is granted access to that resource.
333              
334             =item
335              
336             the user or a group containing the user is granted access to a resource which is a
337             prefix of the requested resource. i.e.
338              
339             / (action): username
340              
341             would grant access to "username" to perform "action" on any resource.
342              
343             =item
344              
345             Additionally, given a user, an action, and a regular expression, it is possible to find
346             I<all> of the resources matching that regular expression for which the user has access. This
347             can be done by sending a GET request to
348              
349             /authz/resources/user/action/regex
350              
351             =item
352              
353             Host-based authorization is also possible -- sending a get
354             request to
355              
356             /host/host/trusted
357              
358             where ".host" is a string representing a hostname, returns
359             200 if the host-based authorization should succeed, and
360             403 otherwise.
361              
362             =back
363              
364             For a complete list of the available routes and what they return see L<PlugAuth::Routes>.
365              
366             =head2 CONFIGURATION
367              
368             Server configuration is done in ~/etc/PlugAuth.conf which is a
369             Clustericious::Config style file. The configuration depends on which plugins you
370             choose, consult your plugin's documentation. The default plugins are
371             L<PlugAuth::Plugin::FlatAuth>, L<PlugAuth::Plugin::FlatAuthz>.
372              
373             Once the authentication and authorization has been configured, PlugAuth
374             can be started (like any L<Mojolicious> or L<Clustericious> application)
375             using the daemon command:
376              
377             % plugauth daemon
378              
379             This will use the built-in web server. To use another web server, additional
380             configuration is required. For example, after adding this:
381              
382             start_mode: hypnotoad
383             hypnotoad :
384             listen : 'http://localhost:8099'
385             env :
386             %# Automatically generated configuration file
387             HYPNOTOAD_CONFIG : /var/run/pluginauth/pluginauth.hypnotoad.conf
388              
389             This start command can be used to start a hypnotoad web server.
390              
391             % plugauth start
392              
393             See L<Clustericious::Config> for more examples, including using with nginx,
394             lighttpd, Plack or Apache.
395              
396             =head1 EVENTS
397              
398             See L<PlugAuth::Routes>
399              
400             =head1 SEE ALSO
401              
402             L<Clustericious::Plugin::PlugAuth>,
403             L<PlugAuth::Client>,
404             L<PlugAuth::Guide::Client>,
405             L<PlugAuth::Guide::Plugin>,
406             L<PlugAuth::Guide::Server>
407              
408             =head1 AUTHOR
409              
410             Graham Ollis <gollis@sesda3.com>
411              
412             =head1 COPYRIGHT AND LICENSE
413              
414             This software is copyright (c) 2012 by NASA GSFC.
415              
416             This is free software; you can redistribute it and/or modify it under
417             the same terms as the Perl 5 programming language system itself.
418              
419             =cut