File Coverage

lib/CallBackery/User.pm
Criterion Covered Total %
statement 28 71 39.4
branch 1 20 5.0
condition 1 33 3.0
subroutine 9 13 69.2
pod 4 5 80.0
total 43 142 30.2


line stmt bran cond sub pod time code
1             package CallBackery::User;
2              
3             # $Id: User.pm 539 2013-12-09 22:28:11Z oetiker $
4              
5             # sorted hashes
6 1     1   7 use Mojo::Base -base, -signatures;
  1         2  
  1         9  
7 1     1   420 use Carp qw(croak confess);
  1         5  
  1         86  
8 1     1   8 use Mojo::Util qw(b64_decode b64_encode secure_compare);
  1         2  
  1         78  
9 1     1   7 use Mojo::JSON qw(encode_json decode_json);
  1         2  
  1         64  
10 1     1   6 use CallBackery::Exception qw(mkerror);
  1         2  
  1         58  
11 1     1   7 use Time::HiRes qw(gettimeofday);
  1         3  
  1         23  
12 1     1   111 use Mojo::Util qw(hmac_sha1_sum);
  1         3  
  1         4342  
13              
14             =head1 NAME
15              
16             CallBackery::User - tell me about the current user
17              
18             =head1 SYNOPSIS
19              
20             use CallBackery::User;
21             my $user = CallBackery::User->new($self->controller);
22              
23             $user->werk;
24             $user->may('right'); # does the user have the given right
25             $user->id;
26              
27             =head1 DESCRIPTION
28              
29             All the methods if L as well as the following
30              
31             =head2 $self->controller
32              
33             the controller
34              
35             =cut
36              
37             has controller => undef, weak => 1;
38              
39             has app => sub ($self) {
40             $self->controller->app;
41             }, weak => 1;
42              
43             has log => sub ($self) {
44             $self->controller ? $self->controller->log : $self->app->log;
45             }, weak => 1;
46              
47             =head2 $self->userId
48              
49             By default the userId is numeric and represents a user account. For system tasks, it gets set to alphabetic identifiers.
50             The following alphabetic identifiers do exist:
51              
52             __CONSOLE when running in the config console mode
53             __CONFIG for backup and restore tasks
54              
55             =cut
56              
57              
58              
59              
60             =head2 userId
61              
62             return the user id if the session user is valid.
63              
64             =cut
65              
66             has userId => sub {
67             my $self = shift;
68             my $cookieUserId = $self->cookieConf->{u};
69             my $db = $self->mojoSqlDb;
70             my $userInfo = $self->db->fetchRow('cbuser',{id=>$cookieUserId});
71             if (my $userId = $userInfo->{cbuser_id}){
72             $self->userInfo($userInfo);
73             $self->db->userName($userInfo->{cbuser_login});
74             return $userId;
75             }
76             my $userCount = [$db->dbh->selectrow_array('SELECT count(cbuser_id) FROM '
77             . $db->dbh->quote_identifier("cbuser"))]->[0];
78             return ($userCount == 0 ? '__ROOT' : undef );
79             };
80              
81              
82             has db => sub {
83             shift->app->database;
84             };
85              
86             =head2 $self->mojoSqlDb
87              
88             returns a pointer to one of the Database object of a Mojo::Pg instance.
89             =cut
90              
91             sub mojoSqlDb {
92 2     2 1 6 shift->db->mojoSqlDb;
93             };
94              
95             =head2 $self->userInfo
96              
97             returns a hash of information about the current user.
98              
99             =cut
100              
101             has userInfo => sub {
102             my $self = shift;
103             my $userId = $self->userId // return {};
104             if ($userId eq '__ROOT'){
105             return {cbuser_id => '__ROOT'};
106             }
107             if ($userId eq '__SHELL'){
108             return {cbuser_id => '__SHELL'};
109             }
110             $self->db->fetchRow('cbuser',{id=>$self->userId}) // {};
111             };
112              
113              
114             =head2 $self->loginName
115              
116             returns a human readable login name for the current user
117              
118             =cut
119              
120             has loginName => sub {
121             shift->userInfo->{cbuser_login} // '*UNKNOWN*';
122             };
123              
124              
125             =head2 $self->sessionConf
126              
127             Extracts the session config from the cookie from the X-Session-Cookie header or the xsc parameter.
128             If the xsc parameter is set, its timestamp must be no older than 2 seconds.
129              
130             =cut
131              
132             has headerSessionCookie => sub {
133             my $self = shift;
134             my $c = $self->controller;
135             return $c->req->headers->header('X-Session-Cookie');
136             };
137              
138             has paramSessionCookie => sub {
139             my $self = shift;
140             my $c = $self->controller;
141             return $c->param('xsc');
142             };
143              
144             has firstSecret => sub {
145             shift->app->secrets()->[0];
146             };
147              
148             sub isUserAuthenticated {
149 0     0 0 0 my $self = shift;
150 0 0       0 $self->userInfo->{cbuser_id} ? 1 : 0;
151             };
152              
153             has cookieConf => sub {
154             my $self = shift;
155             my $headerCookie = $self->headerSessionCookie;
156             my $paramCookie = $self->paramSessionCookie;
157              
158             my ($data,$check) = split /:/,($headerCookie || $paramCookie || ''),2;
159              
160             return {} if not ($data and $check);
161              
162             my $secret = $self->firstSecret;
163              
164             my $checkTest = Mojo::Util::hmac_sha1_sum($data, $secret);
165             if (not secure_compare($check,$checkTest)){
166             $self->log->debug(qq{Bad signed cookie possible hacking attempt.});
167             return {};
168             }
169              
170             my $conf = eval {
171             local $SIG{__DIE__};
172             decode_json(b64_decode($data))
173             };
174             if ($@){
175             $self->log->debug("Invalid cookie structure in '$data': $@");
176             return {};
177             }
178              
179             if (ref $conf ne 'HASH'){
180             $self->log->debug("Cookie structure not a hash");
181             return {};
182             }
183              
184             if (not $conf->{t}){
185             $self->log->debug("Cookie timestamp is invalid");
186             return {};
187             }
188              
189             if ($paramCookie and gettimeofday() - $conf->{t} > 300.0){
190             $self->log->debug(qq{Cookie is expired});
191             die mkerror(38445,"cookie has expired");
192             }
193              
194             return $conf;
195             };
196              
197             =head2 $user->login($login,$password)
198              
199             login the user object. If login return 1 you can then makeSessionCookie.
200              
201             =cut
202              
203             sub login {
204 0     0 1 0 my $self = shift;
205 0         0 my $login = shift;
206 0         0 my $password = shift;
207 0         0 my $cfg = $self->app->config->cfgHash;
208 0   0     0 my $remoteAddress = eval { $self->controller->tx->remote_address } // 'UNKNOWN_IP';
  0         0  
209 0 0 0     0 if ($cfg->{sesame_pass} and $cfg->{sesame_user}
      0        
      0        
      0        
      0        
210             and $login and $password
211             and $login eq $cfg->{sesame_user}
212             and hmac_sha1_sum($password) eq $cfg->{sesame_pass}){
213 0         0 $self->log->info("SESAME Login for $login from $remoteAddress successful");
214 0         0 $self->session(userId=>'__ROOT');
215 0         0 return 1;
216             }
217              
218 0         0 my $db = $self->db;
219 0         0 my $userData = $db->fetchRow('cbuser',{login=>$login});
220 0 0       0 if (not $userData) {
221 0         0 $self->log->info("Login attempt with unknown user $login from $remoteAddress failed");
222 0         0 return undef;
223             }
224              
225 0 0 0     0 if ($userData->{cbuser_password} and $password
      0        
226             and hmac_sha1_sum($password) eq $userData->{cbuser_password} ){
227 0         0 $self->userId($userData->{cbuser_id});
228 0         0 $self->log->info("Login for $login from $remoteAddress successful");
229 0         0 return 1;
230             }
231 0         0 $self->log->info("Login attempt with wrong password for $login from $remoteAddress failed");
232 0         0 return undef;
233             }
234              
235             =head2 $bool = $self->C(right);
236              
237             Check if the user has the right indicated.
238              
239             =cut
240              
241             sub may {
242 0     0 1 0 my $self = shift;
243 0         0 my $right = shift;
244             # root has all the rights
245 0 0 0     0 if (($self->userId // '') eq '__ROOT'){
246 0         0 return 1;
247             }
248 0         0 my $db = $self->db;
249 0         0 my $rightId = $db->lookUp('cbright','key',$right);
250 0         0 my $userId = $self->userId;
251 0 0       0 return ($db->matchData('cbuserright',{cbuser=>$userId,cbright=>$rightId}) ? 1 : 0);
252             }
253              
254             =head2 makeSessionCookie()
255              
256             Returns a timestamped, signed session cookie containing the current userId.
257              
258             =cut
259              
260             sub makeSessionCookie {
261 0     0 1 0 my $self = shift;
262 0         0 my $timeout = shift;
263 0         0 my $now = gettimeofday;
264 0         0 my $conf = b64_encode(encode_json({
265             u => $self->userId,
266             t => $now,
267             }));
268 0         0 $conf =~ s/\s+//g;
269 0         0 my $secret = $self->firstSecret;
270 0         0 my $check = Mojo::Util::hmac_sha1_sum($conf, $secret);
271 0         0 return $conf.':'.$check;
272             }
273              
274 2     2   5063 sub DESTROY ($self) {
  2         6  
  2         3  
275             # we are only interested in objects that get destroyed during
276             # global destruction as this is a potential problem
277 2   50     11 my $class = ref($self) // "child of ". __PACKAGE__;
278 2 50       9 if (${^GLOBAL_PHASE} ne 'DESTRUCT') {
279             # $self->log->debug($class." DESTROYed");
280 2         43 return;
281             }
282 0 0 0       if ($self && ref $self->log){
283 0 0         $self->log->warn("late destruction of $class object during global destruction") unless $self->{prototype};
284 0           return;
285             }
286 0 0 0       warn "extra late destruction of $class object during global destruction\n" unless ref $self and $self->{prototype};
287             }
288              
289              
290             1;
291             __END__