File Coverage

blib/lib/Auth/ActiveDirectory.pm
Criterion Covered Total %
statement 63 77 81.8
branch 21 32 65.6
condition 2 4 50.0
subroutine 19 21 90.4
pod 10 10 100.0
total 115 144 79.8


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