File Coverage

blib/lib/Nitesi/Account/Manager.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Nitesi::Account::Manager;
2              
3 1     1   44983 use strict;
  1         2  
  1         39  
4 1     1   6 use warnings;
  1         3  
  1         31  
5              
6 1     1   6259 use Moo;
  1         42722  
  1         8  
7              
8 1     1   2615 use Nitesi::Class;
  1         2  
  1         34  
9 1     1   613 use Nitesi::Account::Password;
  0            
  0            
10             use ACL::Lite 0.0002;
11              
12             =head1 NAME
13              
14             Nitesi::Account::Manager - Account Manager for Nitesi Shop Machine
15              
16             =head1 SYNOPSIS
17              
18             $account = Nitesi::Account::Manager->new(provider_sub => \&account_providers,
19             session_sub => \&session);
20              
21             $account->init_from_session;
22              
23             $account->status(login_info => 'Please login before checkout',
24             login_continue => 'checkout');
25              
26             $account->login(username => 'shopper@nitesi.biz', password => 'nevairbe');
27              
28             $account->logout();
29              
30             if ($account->exists('shopper@nitesi.biz')) {
31             $account->password(username => 'shopper@nitesi.biz', password => 'nevairbe');
32             }
33              
34             $account->create(email => 'shopper@nitesi.biz');
35              
36             # use this with caution!
37             $account->become('shopper@nitesi.biz');
38              
39             =head1 DESCRIPTION
40              
41             Nitesi's account manager transparently handles multiple providers for authentication,
42             account data and permissions checks.
43              
44             =head1 METHODS
45              
46             =head2 init
47              
48             Initializer called by instance class method.
49              
50             =cut
51              
52             has password_manager => (
53             is => 'rw',
54             lazy => 1,
55             default => sub {Nitesi::Account::Password->new;},
56             );
57              
58             =head2 providers
59              
60             List with account providers.
61              
62             =cut
63              
64             has providers => (
65             is => 'ro',
66             );
67              
68             has session_sub => (
69             is => 'rw',
70             lazy => 1,
71             default => sub {sub {return 1;}},
72             );
73              
74             sub BUILDARGS {
75             my ($class, %args) = @_;
76             my ($ret, @list, $init);
77              
78             $args{providers} = [];
79            
80             if ($args{provider_sub}) {
81             # retrieve list of providers
82             $ret = $args{provider_sub}->();
83            
84             if (ref($ret) eq 'HASH') {
85             # just one provider
86             @list = ($ret);
87             }
88             elsif (ref($ret) eq 'ARRAY') {
89             @list = @$ret;
90             }
91              
92             # instantiate provider objects
93             for $init (@list) {
94             push @$init, 'crypt', Nitesi::Account::Password->new;
95             push @{$args{providers}}, Nitesi::Class->instantiate(@$init);
96             }
97              
98             delete $args{provider_sub};
99             }
100              
101             return \%args;
102             }
103              
104             =head2 init_from_session
105              
106             Reads user information through session routine.
107              
108             =cut
109              
110             sub init_from_session {
111             my $self = shift;
112              
113             $self->{account} = $self->{session_sub}->()
114             || {uid => 0, username => '', roles => []};
115              
116             $self->{acl} = ACL::Lite->new(permissions => $self->{account}->{permissions});
117              
118             return;
119             }
120              
121             =head2 login
122              
123             Perform login. Returns 1 in case of success and
124             0 in case of failure.
125              
126             Leading and trailing spaces will be removed from
127             username and password in advance.
128              
129             =cut
130              
131             sub login {
132             my ($self, %args) = @_;
133             my ($success, $acct);
134              
135             $success = 0;
136              
137             # remove leading/trailing spaces from username and password
138             $args{username} =~ s/^\s+//;
139             $args{username} =~ s/\s+$//;
140              
141             $args{password} =~ s/^\s+//;
142             $args{password} =~ s/\s+$//;
143              
144             my $id = 0;
145              
146             for my $p (@{$self->{providers}}) {
147             if ($acct = $p->login(%args)) {
148             $acct->{provider_id} = $id;
149             $self->session_sub->('init', $acct);
150             $self->{account} = $acct;
151             $self->{acl} = ACL::Lite->new(permissions => $self->{account}->{permissions},
152             uid => $acct->{uid});
153             $success = 1;
154             last;
155             }
156             $id++;
157             }
158              
159             return $success;
160             }
161              
162             =head2 logout
163              
164             Perform logout.
165              
166              
167             B
168              
169             $account->logout();
170              
171             =cut
172              
173             sub logout {
174             my ($self, %args) = @_;
175             my ($provider);
176              
177             # log out if the user is authenticated, so skip it if uid is 0 (as
178             # per doc of uid).
179             if ($self->uid) {
180             $provider = $self->{providers}->[$self->{account}->{provider_id}];
181              
182             if ($provider->can('logout')) {
183             $self->{providers}->[$self->{account}->{provider_id}]->logout;
184             }
185              
186             delete $self->{account};
187             $self->{acl} = ACL::Lite->new;
188             }
189              
190             $self->session_sub->('destroy');
191             }
192              
193             =head2 create
194              
195             Creates account and returns uid for the new account
196             in case of success.
197              
198             B
199              
200             $uid = $account->create(email => 'shopper@nitesi.biz');
201              
202             The password is automatically generated unless you pass it to
203             this method.
204              
205             B
206              
207             $uid = $account->create(email => 'shopper@nitesi.biz',
208             password => 'nevairbe');
209              
210             =cut
211              
212             sub create {
213             my ($self, %args) = @_;
214             my ($password, $uid);
215            
216             # remove leading/trailing spaces from arguments
217             for my $name (keys %args) {
218             if (defined $args{$name}) {
219             $args{$name} =~ s/^\s+//;
220             $args{$name} =~ s/\s+$//;
221             }
222             }
223              
224             unless (exists $args{username} && $args{username} =~ /\S/) {
225             $args{username} = lc($args{email});
226             }
227              
228             # password is added after account creation
229             unless ($password = delete $args{password}) {
230             $password = $self->password_manager->make_password;
231             }
232              
233             for my $p (@{$self->{providers}}) {
234             next unless $p->can('create');
235              
236             if ($p->exists($args{username})) {
237             die "Account already exists: ", $args{username};
238             }
239              
240             if ($uid = $p->create(%args)) {
241             $self->password(username => $args{username},
242             password => $password);
243             last;
244             }
245             }
246              
247             return $uid;
248             }
249              
250             =head2 delete
251              
252             Delete account.
253              
254             B
255              
256             $account->delete('333');
257              
258             =cut
259              
260             sub delete {
261             my ($self, $uid, $p);
262              
263             $self = shift;
264              
265             if (@_) {
266             $uid = shift;
267             }
268             else {
269             $uid = $self->uid;
270             }
271              
272             for $p (@{$self->{providers}}) {
273             if ($p->load($uid)) {
274             return $p->delete($uid);
275             }
276             }
277              
278             return;
279             }
280              
281             =head2 uid
282              
283             Retrieve user identifier of the current user, returns 0 if current user
284             isn't authenticated.
285              
286             B
287              
288             $account->uid();
289              
290             =cut
291              
292             sub uid {
293             my $self = shift;
294              
295             return $self->{account}->{uid} || 0;
296             }
297              
298             =head2 username
299              
300             Retrieve username of the current user. Returns empty string if current user
301             isn't authenticated. If you want to retrieve other user username, use $account->load.
302              
303             B
304              
305             $account->username();
306              
307             =cut
308              
309             sub username {
310             my $self = shift;
311              
312             return $self->{account}->{username};
313             }
314              
315             =head2 roles
316              
317             Retrieve roles of current user.
318              
319             B
320              
321             $account->roles();
322              
323             =cut
324              
325             sub roles {
326             my $self = shift;
327              
328             wantarray ? @{$self->{account}->{roles}} : $self->{account}->{roles};
329             }
330              
331             =head2 has_role
332              
333             Returns true if user is a member of the given role.
334              
335             B
336              
337             if ($account->has_role('admin') { print "Congratulations, you are the admin" };
338              
339             =cut
340              
341             sub has_role {
342             my ($self, $role) = @_;
343              
344             grep {$role eq $_} @{$self->{account}->{roles}};
345             }
346              
347             =head2 permissions
348              
349             Returns permissions as hash reference:
350              
351             $perms = $account->permissions;
352              
353             Returns permissions as list:
354              
355             @perms = $account->permissions;
356              
357             =cut
358              
359             sub permissions {
360             my ($self) = @_;
361              
362             return $self->{acl}->permissions;
363             }
364              
365             =head2 status
366              
367             Helps you to redirect users properly on pages available only to authenticated users.
368              
369             B Before login - Page available only if you are logged in (Step 1)
370              
371             You are not logged in. You are on a page which is available only to those logged in.
372             You set the message for users not logged in and url of the page where you send them after successful login.
373              
374             $account->status(login_info => 'Please login before checkout', login_continue => 'checkout');
375              
376             B At Login page (Step 2)
377              
378             You retrieve the login message to make clear to user why they need to login (to access the page from step 1)
379              
380             $account->status('login_info');
381              
382             B After login (Step 3)
383              
384             Retrieve the login_continue URL and send user to that URL (using redirect or something similar).
385              
386             $account->status('login_continue');
387              
388             =cut
389              
390             sub status {
391             my ($self, @args) = @_;
392              
393             if (@args > 1) {
394             # update status information
395             $self->{account} = $self->session_sub->('update', {@args});
396             }
397             elsif (@args == 1) {
398             if (exists $self->{account}->{$args[0]}) {
399             return $self->{account}->{$args[0]};
400             }
401             else {
402             return '';
403             }
404             }
405             }
406              
407             =head2 exists
408              
409             Check whether account exists.
410              
411             B
412              
413             if ($account->exists('shopper@nitesi.biz')) {
414             print "Account exists\n";
415             }
416              
417             =cut
418              
419             sub exists {
420             my ($self, $username) = @_;
421              
422             return unless defined $username && $username =~ /\S/;
423              
424             for my $p (@{$self->{providers}}) {
425             if ($p->exists($username)) {
426             return $p;
427             }
428             }
429             }
430              
431             =head2 load
432              
433             Returns account data for a given uid as hash.
434              
435             B
436              
437             $account->load('333');
438              
439             =cut
440              
441             sub load {
442             my ($self, $uid) = @_;
443             my ($data);
444              
445             for my $p (@{$self->{providers}}) {
446             if ($data = $p->load($uid)) {
447             return $data;
448             }
449             }
450             }
451              
452             =head2 password
453              
454             Changes password for current account:
455              
456             $account->password('nevairbe');
457              
458             Changes password for other account:
459              
460             $account->password(username => 'shopper@nitesi.biz',
461             password => 'nevairbe');
462              
463             =cut
464              
465             sub password {
466             my $self = shift;
467             my ($provider, %args);
468              
469             if (@_ == 1) {
470             # new password only
471             unless ($self->{account}->{username}) {
472             die "Cannot change password for anonymous user";
473             }
474              
475             $args{username} = $self->{account}->{username};
476             $args{password} = shift;
477             }
478             else {
479             %args = @_;
480              
481             unless ($provider = $self->exists($args{username})) {
482             die "Cannot change password for user $args{username}.";
483             }
484             }
485              
486             $provider->password($self->password_manager->password($args{password}),
487             $args{username});
488             }
489              
490             =head2 acl
491              
492             ACL (Access list) check, see L for details.
493              
494             B
495              
496             if ( $account->acl( check => 'view_prices') {
497             print "You can see prices";
498             }
499              
500              
501             B
502              
503             If you check multiple permissions at once, only one has to granted.
504             The check will return the name of the first granted one in the list (left to right).
505              
506             if ( $account->acl( check => [ qw/admin luka/ ] ) {
507             print "This is Luka's account. Only Luka and administrators can see it".
508             }
509              
510             =cut
511              
512             sub acl {
513             my ($self, $function, @args) = @_;
514              
515             if ($self->{acl}) {
516             if ($function eq 'check') {
517             $self->{acl}->check(@args);
518             }
519             }
520             }
521              
522             =head2 value
523              
524             Retrieve or set account data.
525              
526             B Retrieve city
527              
528             $city = $account->value( 'city');
529              
530             B Set city
531              
532             $city = $account->value( city => 'Ljubljana');
533              
534             =cut
535              
536             sub value {
537             my ($self, $name, $value) = @_;
538              
539             if (@_ == 3) {
540             # update value
541             my ($username, $provider);
542              
543             $username = $self->{account}->{username};
544              
545             unless ($provider = $self->exists($username)) {
546             die "Cannot change value $name for user $username.";
547             }
548              
549             $provider->value($username, $name, $value);
550             $self->{account} = $self->session_sub->('update', {$name => $value});
551              
552             return $value;
553             }
554              
555             if (exists $self->{account}->{$name}) {
556             return $self->{account}->{$name};
557             }
558             }
559              
560             =head2 last_login
561              
562             Returns time of last login (before the current one) in seconds
563             since epoch or undef if provider doesn't supply this information.
564              
565             =cut
566              
567             sub last_login {
568             my ($self) = @_;
569              
570             return $self->{account}->{last_login};
571             }
572              
573             =head2 become
574              
575             Become any user you want:
576            
577             $acct->become('shopper@nitesi.biz');
578              
579             Please use this method with caution.
580              
581             Some parts of the system (DBI, LDAP,...) may choose not to support this method.
582              
583             =cut
584              
585             sub become {
586             my ($self, $username) = @_;
587             my ($p, $acct);
588              
589             my $id = 0;
590              
591             for $p (@{$self->{providers}}) {
592             if ($p->can('become')) {
593             if ($acct = $p->become($username)) {
594             $acct->{provider_id} = $id;
595             $self->session_sub->('init', $acct);
596             $self->{account} = $acct;
597             $self->{acl} = ACL::Lite->new(permissions => $self->{account}->{permissions},
598             uid => $acct->{uid});
599             return 1;
600             }
601             }
602              
603             $id++;
604             }
605             }
606              
607             =head1 AUTHOR
608              
609             Stefan Hornburg (Racke),
610              
611             =head1 LICENSE AND COPYRIGHT
612              
613             Copyright 2011-2013 Stefan Hornburg (Racke) .
614              
615             This program is free software; you can redistribute it and/or modify it
616             under the terms of either: the GNU General Public License as published
617             by the Free Software Foundation; or the Artistic License.
618              
619             See http://dev.perl.org/licenses/ for more information.
620              
621             =cut
622              
623             1;