File Coverage

blib/lib/Auth/ActiveDirectory.pm
Criterion Covered Total %
statement 60 81 74.0
branch 21 34 61.7
condition 2 4 50.0
subroutine 18 21 85.7
pod 11 11 100.0
total 112 151 74.1


line stmt bran cond sub pod time code
1             package Auth::ActiveDirectory;
2              
3             =head1 NAME
4              
5             Auth::ActiveDirectory - Authentication module for MS ActiveDirectory
6              
7             =head1 VERSION
8              
9             Version 0.02
10              
11             =cut
12              
13             our $VERSION = '0.02';
14              
15 3     3   203839 use strict;
  3         4  
  3         86  
16 3     3   11 use warnings FATAL => 'all';
  3         5  
  3         116  
17 3     3   1059 use Net::LDAP qw[];
  3         265631  
  3         69  
18 3     3   23 use Net::LDAP::Constant qw[LDAP_INVALID_CREDENTIALS];
  3         4  
  3         2965  
19             my $ErrorCodes = {
20             '525' => { error => 'user not found' },
21             '52e' => { error => 'invalid credentials' },
22             '530' => { error => 'not permitted to logon at this time' },
23             '531' => { error => 'not permitted to logon at this workstation' },
24             '532' => { error => 'password expired' },
25             '533' => { error => 'data 533' },
26             '701' => { error => 'account expired' },
27             '773' => { error => 'user must reset password' },
28             '775' => { error => 'user account locked' },
29             '534' => {
30             error => 'account disabled',
31             description => 'The user has not been granted the requested logon type at this machine'
32             },
33             };
34              
35             =head1 SUBROUTINES/METHODS
36              
37             =cut
38              
39             {
40              
41             =head2 _ad2unixtimestamp
42              
43             This value represents the number of 100-nanosecond intervals since January 1, 1601 (UTC).
44             https://msdn.microsoft.com
45              
46             ad_timestamp / nanoseconds - offset to 1601
47              
48             =cut
49              
50 1     1   51 sub _ad2unixtimestamp { $_[0] / 10000000 - 11644473600 }
51              
52             =head2 _create_connection
53              
54             =cut
55              
56             sub _create_connection {
57 1     1   2 my ( $host, $port, $timeout ) = @_;
58             return Net::LDAP->new( $host, port => $port || 389, timeout => $timeout || 60 ) || sub {
59 0     0   0 die qq/Failed to connect to '$host'. Reason: '$@'/;
60 0         0 return;
61 1   50     13 };
62             }
63              
64             =head2 _v_is_error
65              
66             =cut
67              
68             sub _v_is_error {
69 2     2   6 my ( $message, $s_user ) = @_;
70 2 50       12 return 0 if ( !$message->is_error );
71 0         0 my $error = $message->error;
72 0 0       0 my $level = $message->code == LDAP_INVALID_CREDENTIALS ? 'debug' : 'error';
73 0         0 die qq/Failed to authenticate user '$s_user'. Reason: '$error'/;
74 0         0 return 1;
75             }
76              
77             =head2 _parse_error_message
78              
79             =cut
80              
81             sub _parse_error_message {
82 0     0   0 my ($message) = @_;
83 0         0 my ($errorcode) = $message->{errorMessage} =~ m/(?:data\s(.*)),/;
84 0         0 return $ErrorCodes->{$errorcode};
85             }
86              
87             =head2 _search_users
88              
89             =cut
90              
91             sub _search_users {
92 2     2   5 my ( $self, $filter ) = @_;
93 2         5 return $self->ldap->search( base => $self->base, filter => $filter );
94             }
95              
96             }
97              
98             =head2 new
99              
100             Constructor
101              
102             =cut
103              
104             sub new {
105 2     2 1 19857 my $class = shift;
106 2         8 my $self = {@_};
107 2         4 bless $self, $class;
108 2 50       18 $self->{base} = qq/dc=$self->{domain},dc=$self->{principal}/ unless $self->{base};
109 2 100       8 $self->{ldap} = _create_connection( $self->{host}, $self->{port}, $self->{timeout} ) unless $self->{ldap};
110 2         1141 return $self;
111             }
112              
113             =head2 authenticate
114              
115             Basicaly the subroutine for authentication in the ActiveDirectory
116              
117             =cut
118              
119             sub authenticate {
120 1     1 1 8 my ( $self, $username, $password ) = @_;
121 1 50       4 return unless $self->ldap;
122 1         4 my $user = sprintf( '%s@%s', $username, $self->domain );
123 1         3 my $message = $self->ldap->bind( $user, password => $password );
124 1 50       925 if ( _v_is_error( $message, $user ) ) {
125 0         0 $self->error_message( _parse_error_message($message) );
126 0         0 return;
127             }
128              
129 1         17 my $result = $self->_search_users( qq/(&(objectClass=person)(userPrincipalName=$user./ . $self->principal . '))' );
130 1         3591 foreach ( $result->entries ) {
131 1         382 require Auth::ActiveDirectory::Group;
132 1         369 require Auth::ActiveDirectory::User;
133             return Auth::ActiveDirectory::User->new(
134             uid => $username,
135             user => $user,
136             firstname => $_->get_value(q/givenName/),
137             surname => $_->get_value(q/sn/),
138             display_name => $_->get_value(q/displayName/),
139             mail => $_->get_value(q/mail/),
140             last_password_set => _ad2unixtimestamp( $_->get_value('pwdLastSet') ),
141              
142             # A value of 0 or 0x7FFFFFFFFFFFFFFF (9223372036854775807) indicates that the account never expires.
143             # https://msdn.microsoft.com/en-us/library/ms675098(v=vs.85).aspx
144             account_expires => ( $_->get_value('accountExpires') != 9223372036854775807 ) ? _ad2unixtimestamp( $_->get_value('accountExpires') ) : undef,
145 1 50       8 groups => [ map { m/^CN=(.*),OU=.*$/ ? Auth::ActiveDirectory::Group->new( name => $1 ) : () } $_->get_value(q/memberOf/) ],
  4 50       52  
146              
147             );
148             }
149 0         0 return;
150             }
151              
152             =head2 list_users
153              
154             =cut
155              
156             sub list_users {
157 1     1 1 456 my ( $self, $user, $password, $search_string ) = @_;
158 1   50     40 my $connection = $self->ldap || return;
159 1         9 my $message = $connection->bind( $user, password => $password );
160 1 50       182 if ( _v_is_error( $message, $user ) ) {
161 0         0 $self->error_message( _parse_error_message($message) );
162 0         0 return;
163             }
164 1         58 my $result = $self->_search_users(qq/(&(objectClass=person)(name=$search_string*))/);
165 1         2303 return [ map { Auth::ActiveDirectory::User->new( name => $_->get_value(q/name/), uid => $_->get_value(q/sAMAccountName/) ) } $result->entries ];
  2         17  
166             }
167              
168             =head2 host
169              
170             Getter/Setter for internal hash key host.
171              
172             =cut
173              
174             sub host {
175 2 100   2 1 10 return $_[0]->{host} unless $_[1];
176 1         3 $_[0]->{host} = $_[1];
177 1         3 return $_[0]->{host};
178             }
179              
180             =head2 port
181              
182             Getter/Setter for internal hash key port.
183              
184             =cut
185              
186             sub port {
187 2 100   2 1 9 return $_[0]->{port} unless $_[1];
188 1         2 $_[0]->{port} = $_[1];
189 1         5 return $_[0]->{port};
190             }
191              
192             =head2 timeout
193              
194             Getter/Setter for internal hash key timeout.
195              
196             =cut
197              
198             sub timeout {
199 2 100   2 1 11 return $_[0]->{timeout} unless $_[1];
200 1         4 $_[0]->{timeout} = $_[1];
201 1         4 return $_[0]->{timeout};
202             }
203              
204             =head2 domain
205              
206             Getter/Setter for internal hash key domain.
207              
208             =cut
209              
210             sub domain {
211 3 100   3 1 514 return $_[0]->{domain} unless $_[1];
212 1         3 $_[0]->{domain} = $_[1];
213 1         3 return $_[0]->{domain};
214             }
215              
216             =head2 principal
217              
218             Getter/Setter for internal hash key principal.
219              
220             =cut
221              
222             sub principal {
223 3 100   3 1 23 return $_[0]->{principal} unless $_[1];
224 1         6 $_[0]->{principal} = $_[1];
225 1         9 return $_[0]->{principal};
226             }
227              
228             =head2 ldap
229              
230             Getter/Setter for internal hash key ldap.
231              
232             =cut
233              
234             sub ldap {
235 5 50   5 1 37 return $_[0]->{ldap} unless $_[1];
236 0         0 $_[0]->{ldap} = $_[1];
237 0         0 return $_[0]->{ldap};
238             }
239              
240             =head2 base
241              
242             Getter/Setter for internal hash key base.
243              
244             =cut
245              
246             sub base {
247 2 50   2 1 15 return $_[0]->{base} unless $_[1];
248 0           $_[0]->{base} = $_[1];
249 0           return $_[0]->{base};
250             }
251              
252             =head2 error_message
253              
254             Get error message if something is going wrong.
255              
256             =cut
257              
258             sub error_message {
259 0 0   0 1   return $_[0]->{_error_message} unless $_[1];
260 0           $_[0]->{_error_message} = $_[1];
261 0           return $_[0]->{_error_message};
262             }
263              
264             1; # Auth::ActiveDirectory
265              
266             __END__