Split credential checking into a separate method from generic binding
[catagits/Catalyst-Authentication-Store-LDAP.git] / lib / Catalyst / Authentication / Store / LDAP / User.pm
1
2 =pod
3
4 =head1 NAME
5
6 Catalyst::Authentication::Store::LDAP::User
7  - A User object representing an LDAP object. 
8
9 =head1 SYNOPSIS
10
11 You should be creating these objects through L<Catalyst::Authentication::Store::LDAP::Backend>'s "get_user" method, or just letting $c->authenticate do
12 it for you.
13
14     sub action : Local {
15         my ( $self, $c ) = @_;
16         $c->authenticate({
17             id => $c->req->param(username),
18             password => $c->req->param(password)
19         );
20         $c->log->debug($c->user->username . "is really neat!");
21     }
22
23 If you access just $c->user in a scalar context, it will return the current
24 username.
25
26 =head1 DESCRIPTION
27
28 This wraps up an LDAP object and presents a simplified interface to it's
29 contents.  It uses some AUTOLOAD magic to pass method calls it doesn't
30 understand through as simple read only accessors for the LDAP entries
31 various attributes.  
32
33 It gets grumpy if you ask for an attribute via the AUTOLOAD mechanism
34 that it doesn't know about.  Avoid that with using "has_attribute", 
35 discussed in more detail below.
36
37 You can skip all that and just go straight to the L<Net::LDAP::Entry>
38 object through the "ldap_entry" method:
39
40     my $entry = $c->user->ldap_entry;
41
42 It also has support for Roles.
43
44 =cut
45
46 package Catalyst::Authentication::Store::LDAP::User;
47 use base qw( Catalyst::Authentication::User Class::Accessor::Fast );
48
49 use strict;
50 use warnings;
51 use Scalar::Util qw/refaddr/;
52
53 our $VERSION = '1.014';
54
55 BEGIN { __PACKAGE__->mk_accessors(qw/user store/) }
56
57 use overload '""' => sub { shift->stringify }, fallback => 1;
58
59 my %_ldap_connection_passwords; # Store inside-out so that they don't show up
60                                 # in dumps..
61
62 =head1 METHODS
63
64 =head2 new($store, $user, $c)
65
66 Takes a L<Catalyst::Authentication::Store::LDAP::Backend> object
67 as $store, and the data structure returned by that class's "get_user"
68 method as $user.  The final argument is an instance of your application,
69 which is passed along for those wanting to subclass User and perhaps use
70 models for fetching data.
71
72 Returns a L<Catalyst::Authentication::Store::LDAP::User> object.
73
74 =cut
75
76 sub new {
77     my ( $class, $store, $user, $c ) = @_;
78
79     return unless $user;
80
81     bless { store => $store, user => $user, }, $class;
82 }
83
84 =head2 id
85
86 Returns the results of the "stringify" method.
87
88 =cut
89
90 sub id {
91     my $self = shift;
92     return $self->stringify;
93 }
94
95 =head2 stringify
96
97 Uses the "user_field" configuration option to determine what the "username"
98 of this object is, and returns it.
99
100 If you use the special value "dn" for user_field, it will return the DN
101 of the L<Net::LDAP::Entry> object.
102
103 =cut
104
105 sub stringify {
106     my ($self) = @_;
107     my $userfield = $self->store->user_field;
108     $userfield = $$userfield[0] if ref $userfield eq 'ARRAY';
109     if ( $userfield eq "dn" ) {
110         my ($string) = $self->user->ldap_entry->dn;
111         return $string;
112     }
113     else {
114         my $val = $self->$userfield;
115         return ref($val) eq 'ARRAY' ? $val->[0] : $val;
116     }
117 }
118
119 =head2 supported_features
120
121 Returns hashref of features that this Authentication::User subclass supports.
122
123 =cut
124
125 sub supported_features {
126     return {
127         password => { self_check => 1, },
128         session  => 1,
129         roles    => { self_check => 0, },
130     };
131 }
132
133 =head2 check_password($password)
134
135 Bind's to the directory as the DN of the internal L<Net::LDAP::Entry> object,
136 using the bind password supplied in $password.  Returns 1 on a successful
137 bind, 0 on failure.
138
139 =cut
140
141 sub check_password {
142     my ( $self, $password ) = @_;
143     if ( $self->store->ldap_auth($self->ldap_entry->dn, $password) ) {
144         # Stash a closure which can be used to retrieve the connection in the users context later.
145         $_ldap_connection_passwords{refaddr($self)} = $password;
146         return 1;
147     }
148     else {
149         return 0;
150     }
151 }
152
153 =head2 roles
154
155 Returns the results of L<Catalyst::Authentication::Store::LDAP::Backend>'s "lookup_roles" method, an array of roles that are valid for this user.
156
157 =cut
158
159 sub roles {
160     my $self = shift;
161     $self->{_roles} ||= [$self->store->lookup_roles($self)];
162     return @{$self->{_roles}};
163 }
164
165 =head2 for_session
166
167 Returns the User object, stringified.
168
169 =cut
170
171 sub for_session {
172     my $self = shift;
173     return $self->stringify;
174 }
175
176 =head2 ldap_entry
177
178 Returns the raw ldap_entry. 
179
180 =cut
181
182 sub ldap_entry {
183     my $self = shift;
184     return $self->user->{'ldap_entry'};
185 }
186
187 =head2 attributes($type)
188
189 Returns an array of attributes present for this user.  If $type is "ashash",
190 it will return a hash with the attribute names as keys. (And the values of
191 those attributes as, well, the values of the hash)
192
193 =cut
194
195 sub attributes {
196     my ( $self, $type ) = @_;
197     if ( $type eq "ashash" ) {
198         return $self->user->{'attributes'};
199     }
200     else {
201         return keys( %{ $self->user->{'attributes'} } );
202     }
203 }
204
205 =head2 has_attribute
206
207 Returns the values for an attribute, or undef if that attribute is not present.
208 The safest way to get at an attribute. 
209
210 =cut
211
212 sub has_attribute {
213     my ( $self, $attribute ) = @_;
214     if ( !defined($attribute) ) {
215         Catalyst::Exception->throw(
216             "You must provide an attribute to has_attribute!");
217     }
218     if ( $attribute eq "dn" ) {
219         return $self->ldap_entry->dn;
220     }
221     elsif ( $attribute eq "username" ) {
222        return $self->user->{'attributes'}->{$self->store->user_field};
223     }
224     elsif ( exists( $self->user->{'attributes'}->{$attribute} ) ) {
225         return $self->user->{'attributes'}->{$attribute};
226     }
227     else {
228         return undef;
229     }
230 }
231
232 =head2 get
233
234 A simple wrapper around has_attribute() to satisfy the Catalyst::Authentication::User API.
235
236 =cut
237
238 sub get { return shift->has_attribute(@_) }
239
240 =head2 get_object
241
242 Satisfies the Catalyst::Authentication::User API and returns the contents of the user()
243 attribute.
244
245 =cut
246
247 sub get_object { return shift->user }
248
249 =head2 ldap_connection
250
251 Re-binds to the auth store with the credentials of the user you logged in
252 as, and returns a L<Net::LDAP> object which you can use to do further queries.
253
254 =cut
255
256 sub ldap_connection {
257     my $self = shift;
258     $self->store->ldap_bind( undef, $self->ldap_entry->dn,
259         $_ldap_connection_passwords{refaddr($self)} );
260 }
261
262 =head2 AUTOLOADed methods
263
264 We automatically map the attributes of the underlying L<Net::LDAP::Entry>
265 object to read-only accessor methods.  So, if you have an entry that looks
266 like this one:
267
268     dn: cn=adam,ou=users,dc=yourcompany,dc=com
269     cn: adam
270     loginShell: /bin/zsh
271     homeDirectory: /home/adam
272     gecos: Adam Jacob
273     gidNumber: 100
274     uidNumber: 1053
275     mail: adam@yourcompany.com
276     uid: adam
277     givenName: Adam
278     sn: Jacob
279     objectClass: inetOrgPerson
280     objectClass: organizationalPerson
281     objectClass: Person
282     objectClass: Top
283     objectClass: posixAccount
284
285 You can call:
286
287     $c->user->homedirectory
288
289 And you'll get the value of the "homeDirectory" attribute.  Note that
290 all the AUTOLOADed methods are automatically lower-cased. 
291
292 =head2 Special Keywords
293
294 The highly useful and common method "username" will map to the configured
295 value of user_field (uid by default.) 
296
297     $c->user->username == $c->user->uid
298
299 =cut
300
301 sub DESTROY {
302     my $self = shift;
303     # Don't leak passwords..
304     delete $_ldap_connection_passwords{refaddr($self)};
305 }
306
307 sub can {
308     my ($self, $method) = @_;
309
310     return $self->SUPER::can($method) || do {
311         return unless $self->has_attribute($method);
312         return sub { $_[0]->has_attribute($method) };
313     };
314 }
315
316 sub AUTOLOAD {
317     my $self = shift;
318
319     ( my $method ) = ( our $AUTOLOAD =~ /([^:]+)$/ );
320
321     if ( $method eq "DESTROY" ) {
322         return;
323     }
324
325     if ( my $attribute = $self->has_attribute($method) ) {
326         return $attribute;
327     }
328     else {
329         Catalyst::Exception->throw(
330             "No attribute $method for User " . $self->stringify );
331     }
332 }
333
334 1;
335
336 __END__
337
338 =head1 AUTHORS
339
340 Adam Jacob <holoway@cpan.org>
341
342 Some parts stolen shamelessly and entirely from
343 L<Catalyst::Plugin::Authentication::Store::Htpasswd>. 
344
345 Currently maintained by Peter Karman <karman@cpan.org>.
346
347 =head1 THANKS
348
349 To nothingmuch, ghenry, castaway and the rest of #catalyst for the help. :)
350
351 =head1 SEE ALSO
352
353 L<Catalyst::Authentication::Store::LDAP>, L<Catalyst::Authentication::Store::LDAP::Backend>, L<Catalyst::Plugin::Authentication>, L<Net::LDAP>
354
355 =head1 COPYRIGHT & LICENSE
356
357 Copyright (c) 2005 the aforementioned authors. All rights
358 reserved. This program is free software; you can redistribute
359 it and/or modify it under the same terms as Perl itself.
360
361 =cut
362