File Coverage

blib/lib/Bot/BasicBot/Pluggable/Module/Auth.pm
Criterion Covered Total %
statement 77 79 97.4
branch 37 44 84.0
condition 8 15 53.3
subroutine 10 10 100.0
pod 4 4 100.0
total 136 152 89.4


line stmt bran cond sub pod time code
1             package Bot::BasicBot::Pluggable::Module::Auth;
2             $Bot::BasicBot::Pluggable::Module::Auth::VERSION = '1.10';
3 4     4   13 use base qw(Bot::BasicBot::Pluggable::Module);
  4         5  
  4         243  
4 4     4   15 use warnings;
  4         30  
  4         83  
5 4     4   11 use strict;
  4         5  
  4         74  
6 4     4   1571 use Crypt::SaltedHash;
  4         10541  
  4         2682  
7              
8             sub init {
9 4     4 1 5 my $self = shift;
10 4         24 $self->config(
11             {
12             password_admin => "julia",
13             allow_anonymous => 0,
14             }
15             );
16             # A list of admin commands handled by this module and their usage
17             $self->{_admin_commands} = {
18 4         23 auth => '<username> <password>',
19             adduser => '<username> <password>',
20             deluser => '<username>',
21             password => '<old password> <new password>',
22             users => '',
23             };
24             }
25              
26             sub help {
27 1     1 1 2 my $self = shift;
28             return "Authenticator for admin-level commands. Usage: "
29 5         22 . join ", ", map { "!$_ $self->{_admin_commands}{$_}" }
30 1         3 keys %{ $self->{_admin_commands} };
  1         5  
31             }
32              
33             sub admin {
34 18     18 1 16 my ( $self, $mess ) = @_;
35 18         19 my $body = $mess->{body};
36              
37 18 50 33     75 return unless ( $body and length($body) > 4 );
38              
39             # we don't care about commands that don't start with '!'.
40 18 100       54 return 0 unless $body =~ /^!/;
41              
42             # Find out what the command is:
43 17         53 my ($command, $params) = split '\s+', $mess->{body}, 2;
44 17         40 $command =~ s/^!//;
45 17         20 $command = lc $command;
46 17         16 my @params;
47 17 100       49 @params = split /\s+/, $params if defined $params;
48              
49             # If it's not a command we handle, go no further:
50 17 50       32 return 0 unless exists $self->{_admin_commands}{$command};
51              
52             # Basic usage check: the usage message declares which params are taken, so
53             # check we have the right number:
54 17         18 my $usage_message = $self->{_admin_commands}{$command};
55            
56             # Count how many params we want (assignment to empty list gets us list
57             # context, then assigning to scalar results in the count):
58 17         77 my $want_params = () = $usage_message =~ m{<.+?>}g;
59              
60 17 100       32 if (scalar @params != $want_params) {
61 3         10 return "Usage: !$command $usage_message";
62             }
63              
64             # system commands have to be directly addressed...
65 14 100       31 return 1 unless $mess->{address};
66              
67             # ...and in a privmsg.
68             return "Admin commands in privmsg only, please."
69 13 50 33     67 unless !defined $mess->{channel} || $mess->{channel} eq 'msg';
70              
71 13 100       41 if ($command eq 'auth') {
    100          
    100          
    100          
    50          
72 7         10 my ( $user, $pass ) = @params;
73 7         21 my $stored = $self->get( "password_" . $user );
74              
75 7 100       14 if ( _check_password($pass, $stored) ) {
76 4         404 $self->{auth}{ $mess->{who} }{time} = time();
77 4         7 $self->{auth}{ $mess->{who} }{username} = $user;
78 4 100 66     14 if ( $user eq "admin" and $pass eq "julia" ) {
79             return
80 1         3 "Authenticated. But change the password - you're using the default.";
81             }
82 3         15 return "Authenticated.";
83             }
84             else {
85 3         112 delete $self->{auth}{ $mess->{who} };
86 3         9 return "Wrong password.";
87             }
88             } elsif ( $command eq 'adduser' ) {
89 2         3 my ( $user, $pass ) = @params;
90 2 100       4 if ( $self->authed( $mess->{who} ) ) {
91 1         3 $self->set( "password_" . $user, _hash_password($pass) );
92 1         5 return "Added user $user.";
93             }
94             else {
95 1         3 return "You need to authenticate.";
96             }
97             } elsif ( $command eq 'deluser' ) {
98 2         4 my ($user) = @params;
99 2 100       4 if ( $self->authed( $mess->{who} ) ) {
100 1         11 $self->unset( "password_" . $user );
101 1         4 return "Deleted user $user.";
102             }
103             else {
104 1         3 return "You need to authenticate.";
105             }
106             } elsif ( $command eq 'password' ) {
107 1         3 my ( $old_pass, $pass ) = @params;
108 1 50       3 if ( $self->authed( $mess->{who} ) ) {
109 1         4 my $username = $self->{auth}{ $mess->{who} }{username};
110 1 50       8 if (_check_password($old_pass, $self->get("password_$username")) ) {
111 1         155 $self->set( "password_$username", _hash_password($pass) );
112 1         10 return "Changed password to $pass.";
113             }
114             else {
115 0         0 return "Wrong password.";
116             }
117             }
118             else {
119 0         0 return "You need to authenticate.";
120             }
121             } elsif ( $command eq 'users' ) {
122             return "Users: "
123             . join( ", ",
124 1 50       12 map { my $user = $_; $user =~ s/^password_// ? $user : () }
  1         2  
  1         17  
125             $self->store_keys( res => ["^password"] ) )
126             . ".";
127            
128             }
129            
130             }
131              
132             sub authed {
133 12     12 1 17 my ( $self, $username ) = @_;
134             return 1
135             if ( $self->{auth}{$username}{time}
136 12 100 66     62 and $self->{auth}{$username}{time} + 7200 > time() );
137 6         19 return 0;
138             }
139              
140             # Given a password provided by the user and the password stored in the database,
141             # see if they match. Older versions stored plaintext passwords, newer versions
142             # use salted hashed passwords.
143             sub _check_password {
144 8     8   10 my ($entered_pw, $stored_pw) = @_;
145 8 100 66     31 return unless defined $entered_pw && defined $stored_pw;
146 7 100       21 if ($stored_pw =~ /^\{SSHA\}/) {
147 5         17 return Crypt::SaltedHash->validate($stored_pw, $entered_pw);
148             } else {
149 2         5 return $entered_pw eq $stored_pw;
150             }
151             }
152              
153             # Given a plain-text password, return a salted hashed version to store
154             sub _hash_password {
155 2     2   3 my $plain_pw = shift;
156 2         8 my $csh = Crypt::SaltedHash->new(algorithm => 'SHA-1');
157 2         2961 $csh->add($plain_pw);
158 2         23 return $csh->generate;
159             }
160              
161             1;
162             __END__
163              
164             =head1 NAME
165              
166             Bot::BasicBot::Pluggable::Module::Auth - authentication for Bot::BasicBot::Pluggable modules
167              
168             =head1 VERSION
169              
170             version 1.10
171              
172             =head1 SYNOPSIS
173              
174             This module catches messages at priority 1 and stops anything starting
175             with '!' unless the user is authed. Most admin modules, e.g. Loader, can
176             merely sit at priority 2 and assume the user is authed if the !command
177             reaches them. If you want to use modules that can change bot state, like
178             Loader or Vars, you almost certainly want this module.
179              
180             =head1 IRC USAGE
181              
182             The default user is 'admin' with password 'julia'. Change this.
183              
184             =over 4
185              
186             =item !auth <username> <password>
187              
188             Authenticate as an administrators. Logins timeout after an hour.
189              
190             =item !adduser <username> <password>
191              
192             Adds a user with the given password.
193              
194             =item !deluser <username>
195              
196             Deletes a user. Don't delete yourself, that's probably not a good idea.
197              
198             =item !password <old password> <new password>
199              
200             Change your current password (must be logged in first).
201              
202             =item !users
203              
204             List all the users the bot knows about.
205              
206             =back
207              
208             =head1 VARIABLES
209              
210             =over 4
211              
212             =item password_admin
213              
214             This variable specifies the admin password. Its normally set via the
215             !password directive and defaults to 'julia'. Please change this as soon
216             as possible.
217              
218             =item allow_anonymous
219              
220             If this variable is true, the implicit authentication handling is
221             disabled. Every module will have to check for authentication via the
222             authed method, otherwise access is just granted. This is only useful
223             to allow modules to handle directives starting with an exclamation
224             mark without needing any authentication. And to make things even more
225             interesting, you won't be warned that you haven't authenticated, so modules
226             needing authentication will fail without any warning. It defaults to
227             false and should probably never be changed. You've been warned.
228              
229             =back
230              
231             =head1 METHODS
232              
233             The only useful method is C<authed()>:
234              
235             =over 4
236              
237             =item authed($username)
238              
239             Returns 1 if the given username is logged in, 0 otherwise:
240              
241             if ($bot->module("Auth")->authed("jerakeen")) { ... }
242              
243             =back
244              
245             =head1 BUGS
246              
247             All users are admins. This is fine at the moment, as the only things that need
248             you to be logged in are admin functions. Passwords are stored in plaintext, and
249             are trivial to extract for any module on the system. I don't consider this a
250             bug, because I assume you trust the modules you're loading. If Auth is I<not>
251             loaded, all users effectively have admin permissions. This may not be a good
252             idea, but is also not an Auth bug, it's an architecture bug.
253              
254             =head1 AUTHOR
255              
256             Mario Domgoergen <mdom@cpan.org>
257              
258             This program is free software; you can redistribute it
259             and/or modify it under the same terms as Perl itself.