|
Lines 1-5
Link Here
|
| 1 |
package C4::Auth_with_ldap; |
1 |
package C4::Auth_with_ldap; |
| 2 |
|
|
|
| 3 |
# Copyright 2000-2002 Katipo Communications |
2 |
# Copyright 2000-2002 Katipo Communications |
| 4 |
# |
3 |
# |
| 5 |
# This file is part of Koha. |
4 |
# This file is part of Koha. |
|
Lines 17-26
package C4::Auth_with_ldap;
Link Here
|
| 17 |
# with Koha; if not, write to the Free Software Foundation, Inc., |
16 |
# with Koha; if not, write to the Free Software Foundation, Inc., |
| 18 |
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
17 |
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| 19 |
|
18 |
|
| 20 |
use strict; |
19 |
use Modern::Perl; |
| 21 |
#use warnings; FIXME - Bug 2505 |
|
|
| 22 |
use Digest::MD5 qw(md5_base64); |
20 |
use Digest::MD5 qw(md5_base64); |
| 23 |
|
|
|
| 24 |
use C4::Debug; |
21 |
use C4::Debug; |
| 25 |
use C4::Context; |
22 |
use C4::Context; |
| 26 |
use C4::Members qw(AddMember changepassword); |
23 |
use C4::Members qw(AddMember changepassword); |
|
Lines 30-63
use C4::Utils qw( :all );
Link Here
|
| 30 |
use List::MoreUtils qw( any ); |
27 |
use List::MoreUtils qw( any ); |
| 31 |
use Net::LDAP; |
28 |
use Net::LDAP; |
| 32 |
use Net::LDAP::Filter; |
29 |
use Net::LDAP::Filter; |
|
|
30 |
use YAML; |
| 33 |
|
31 |
|
| 34 |
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug); |
32 |
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug); |
| 35 |
|
33 |
|
| 36 |
BEGIN { |
34 |
BEGIN { |
| 37 |
require Exporter; |
35 |
require Exporter; |
| 38 |
$VERSION = 3.07.00.049; # set the version for version checking |
36 |
$VERSION = 3.07.00.049; # set the version for version checking |
| 39 |
@ISA = qw(Exporter); |
37 |
@ISA = qw(Exporter); |
| 40 |
@EXPORT = qw( checkpw_ldap ); |
38 |
@EXPORT = qw( checkpw_ldap ); |
|
|
39 |
} |
| 40 |
|
| 41 |
# return the ref of the subroutine |
| 42 |
sub load_subroutine { |
| 43 |
require Package::Stash; |
| 44 |
my ( $module, $sub ) = @_; |
| 45 |
my $stash = Package::Stash->new($module); |
| 46 |
unless ( %{ $stash->namespace } ) { |
| 47 |
eval "require $module"; |
| 48 |
$@ and die $@; |
| 49 |
} |
| 50 |
$stash->get_package_symbol('&'.$sub); |
| 41 |
} |
51 |
} |
| 42 |
|
52 |
|
| 43 |
# Redefine checkpw_ldap: |
53 |
# Redefine checkpw_ldap: |
| 44 |
# connect to LDAP (named or anonymous) |
54 |
# connect to LDAP (named or anonymous) |
| 45 |
# ~ retrieves $userid from KOHA_CONF mapping |
55 |
# ~ retrieves $userid from KOHA_CONF mapping |
| 46 |
# ~ then compares $password with userPassword |
56 |
# ~ then compares $password with userPassword |
| 47 |
# ~ then gets the LDAP entry |
57 |
# ~ then gets the LDAP entry |
| 48 |
# ~ and calls the memberadd if necessary |
58 |
# ~ and calls the memberadd if necessary |
| 49 |
|
59 |
|
| 50 |
sub ldapserver_error { |
60 |
sub ldapserver_error { |
| 51 |
return sprintf('No ldapserver "%s" defined in KOHA_CONF: ' . $ENV{KOHA_CONF}, shift); |
61 |
return sprintf('No ldapserver "%s" defined in KOHA_CONF: ' . $ENV{KOHA_CONF}, shift); |
| 52 |
} |
62 |
} |
| 53 |
|
63 |
|
|
|
64 |
sub debug_msg { $debug and say STDERR @_ } |
| 65 |
sub logger { say STDERR YAML::Dump @_ } |
| 66 |
|
| 54 |
use vars qw($mapping @ldaphosts $base $ldapname $ldappassword); |
67 |
use vars qw($mapping @ldaphosts $base $ldapname $ldappassword); |
| 55 |
my $context = C4::Context->new() or die 'C4::Context->new failed'; |
68 |
my $context = C4::Context->new() or die 'C4::Context->new failed'; |
| 56 |
my $ldap = C4::Context->config("ldapserver") or die 'No "ldapserver" in server hash from KOHA_CONF: ' . $ENV{KOHA_CONF}; |
69 |
my $ldap = C4::Context->config("ldapserver") or die 'No "ldapserver" in server hash from KOHA_CONF: ' . $ENV{KOHA_CONF}; |
| 57 |
my $prefhost = $ldap->{hostname} or die ldapserver_error('hostname'); |
70 |
|
| 58 |
my $base = $ldap->{base} or die ldapserver_error('base'); |
71 |
my ( $prefhost, $base ) = ('')x2; |
| 59 |
$ldapname = $ldap->{user} ; |
72 |
unless ( $$ldap{authmethod} ) { |
| 60 |
$ldappassword = $ldap->{pass} ; |
73 |
say STDERR "deprecated ldap configuration, see documentation"; |
|
|
74 |
$base = $$ldap{base} or die ldapserver_error('base'); |
| 75 |
$prefhost = $$ldap{hostname} or die ldapserver_error('hostname'); |
| 76 |
} |
| 77 |
|
| 78 |
$ldapname = $ldap->{user}; |
| 79 |
$ldappassword = $ldap->{pass}; |
| 61 |
our %mapping = %{$ldap->{mapping}}; # FIXME dpavlin -- don't die because of || (); from 6eaf8511c70eb82d797c941ef528f4310a15e9f9 |
80 |
our %mapping = %{$ldap->{mapping}}; # FIXME dpavlin -- don't die because of || (); from 6eaf8511c70eb82d797c941ef528f4310a15e9f9 |
| 62 |
my @mapkeys = keys %mapping; |
81 |
my @mapkeys = keys %mapping; |
| 63 |
$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys ( total ): ", join ' ', @mapkeys, "\n"; |
82 |
$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys ( total ): ", join ' ', @mapkeys, "\n"; |
|
Lines 65-104
$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys ( total ): ",
Link Here
|
| 65 |
$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (populated): ", join ' ', @mapkeys, "\n"; |
84 |
$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (populated): ", join ' ', @mapkeys, "\n"; |
| 66 |
|
85 |
|
| 67 |
my %config = ( |
86 |
my %config = ( |
| 68 |
anonymous => ($ldapname and $ldappassword) ? 0 : 1, |
87 |
anonymous => ( $ldapname and $ldappassword ) ? 0 : 1, |
| 69 |
replicate => defined($ldap->{replicate}) ? $ldap->{replicate} : 1, # add from LDAP to Koha database for new user |
88 |
# add from LDAP to Koha database for new user |
| 70 |
update => defined($ldap->{update} ) ? $ldap->{update} : 1, # update from LDAP to Koha database for existing user |
89 |
replicate => defined( $ldap->{replicate} ) ? $ldap->{replicate} : 1, |
|
|
90 |
# update from LDAP to Koha database for existing user |
| 91 |
update => defined( $ldap->{update} ) ? $ldap->{update} : 1, |
| 71 |
); |
92 |
); |
| 72 |
|
93 |
|
| 73 |
sub description { |
94 |
sub description { |
| 74 |
my $result = shift or return; |
95 |
my $result = shift or return; |
| 75 |
return "LDAP error #" . $result->code |
96 |
return "LDAP error #" . $result->code |
| 76 |
. ": " . $result->error_name . "\n" |
97 |
. ": " . $result->error_name . "\n" |
| 77 |
. "# " . $result->error_text . "\n"; |
98 |
. "# " . $result->error_text . "\n"; |
| 78 |
} |
99 |
} |
| 79 |
|
100 |
|
| 80 |
sub search_method { |
101 |
sub search_method { |
| 81 |
my $db = shift or return; |
102 |
my $db = shift or return; |
| 82 |
my $userid = shift or return; |
103 |
my $userid = shift or return; |
| 83 |
my $uid_field = $mapping{userid}->{is} or die ldapserver_error("mapping for 'userid'"); |
104 |
my $uid_field = $mapping{userid}->{is} or die ldapserver_error("mapping for 'userid'"); |
| 84 |
my $filter = Net::LDAP::Filter->new("$uid_field=$userid") or die "Failed to create new Net::LDAP::Filter"; |
105 |
my $filter = Net::LDAP::Filter->new("$uid_field=$userid") or die "Failed to create new Net::LDAP::Filter"; |
| 85 |
my $search = $db->search( |
106 |
my $search = $db->search( |
| 86 |
base => $base, |
107 |
base => $base, |
| 87 |
filter => $filter, |
108 |
filter => $filter, |
| 88 |
# attrs => ['*'], |
109 |
# attrs => ['*'], |
| 89 |
) or die "LDAP search failed to return object."; |
110 |
) or die "LDAP search failed to return object."; |
| 90 |
my $count = $search->count; |
111 |
my $count = $search->count; |
| 91 |
if ($search->code > 0) { |
112 |
if ($search->code > 0) { |
| 92 |
warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count) . description($search); |
113 |
warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count) . description($search); |
| 93 |
return 0; |
114 |
return 0; |
| 94 |
} |
115 |
} |
| 95 |
if ($count != 1) { |
116 |
if ($count != 1) { |
| 96 |
warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count); |
117 |
warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count); |
| 97 |
return 0; |
118 |
return 0; |
| 98 |
} |
119 |
} |
| 99 |
return $search; |
120 |
return $search; |
| 100 |
} |
121 |
} |
| 101 |
|
122 |
|
|
|
123 |
# $cnx is an Net::LDAP object that dies when error occurs |
| 124 |
# $search are params for the search method (default base is the one set in the config) |
| 125 |
sub _anon_search { |
| 126 |
my ( $cnx, $search ) = @_; |
| 127 |
|
| 128 |
my $entry; |
| 129 |
for my $branch ( @{ $$ldap{branch} } ) { |
| 130 |
debug_msg "search $$search{filter} at $$branch{dn}"; |
| 131 |
|
| 132 |
my $branch_search = { %$search, base => $$branch{dn}, search => "ObjectClass=*" }; |
| 133 |
$entry = eval { |
| 134 |
$cnx |
| 135 |
->search( %$branch_search ) |
| 136 |
->shift_entry |
| 137 |
}; |
| 138 |
|
| 139 |
if ( $entry ) { |
| 140 |
debug_msg "found ", $entry->dn; |
| 141 |
return { entry => $entry, branch => $branch }; |
| 142 |
} |
| 143 |
elsif ( $@ ) { return { error => 'UNKNOWN', msg => $@ } } |
| 144 |
else { $debug and logger { "failed search" => $branch_search } } |
| 145 |
} |
| 146 |
} |
| 147 |
|
| 148 |
sub set_xattr { |
| 149 |
my ( $id, $borrower ) = @_; |
| 150 |
if ( my $x = $$borrower{xattr} ) { |
| 151 |
#SetBorrowerAttributes is not managing when being sent an Array ref |
| 152 |
my $attrs = [ map { |
| 153 |
my $key = $_; |
| 154 |
my @listattributes; |
| 155 |
if (ref ($x->{$key}) eq "ARRAY"){ |
| 156 |
foreach my $value (@{$x->{$key}}){ |
| 157 |
push @listattributes, { code => $key, value => $value } |
| 158 |
} |
| 159 |
} |
| 160 |
else { |
| 161 |
push @listattributes, { code => $key, value => $$x{$key} } |
| 162 |
} |
| 163 |
@listattributes; |
| 164 |
} keys %$x ]; |
| 165 |
$debug and logger { "creating $id" => $attrs }; |
| 166 |
SetBorrowerAttributes( $id, $attrs ); |
| 167 |
} |
| 168 |
} |
| 169 |
|
| 170 |
sub accept_borrower { |
| 171 |
my ($borrower,$userid) = @_; |
| 172 |
for ( $$borrower{column}{userid} ) { |
| 173 |
$userid ||= $_ or die; |
| 174 |
unless ( $_ ) { |
| 175 |
$_ = $userid; |
| 176 |
next; |
| 177 |
} |
| 178 |
} |
| 179 |
|
| 180 |
my ($id) = exists_local( $userid ) or debug_msg "$userid is newcommer"; |
| 181 |
|
| 182 |
my $newcommer = not $id; |
| 183 |
|
| 184 |
if ( $newcommer ) { |
| 185 |
return 0 unless $config{update}; |
| 186 |
$debug and logger { Member => $$borrower{column} }; |
| 187 |
$id = AddMember( %{ $$borrower{column} } ) or return 0; |
| 188 |
} else { |
| 189 |
if ( $config{replicate} ) { |
| 190 |
delete $$borrower{column}{dateenrolled}; |
| 191 |
delete $$borrower{column}{dateexpiry}; |
| 192 |
my $cardnumber = update_local |
| 193 |
( $userid, $$borrower{column}{password}, $id, $$borrower{column} ); |
| 194 |
if ( my $old_cardnumber = $$borrower{column}{cardnumber} ) { |
| 195 |
if ( $old_cardnumber ne $cardnumber ) { |
| 196 |
warn "update_local returned cardnumber '$cardnumber' instead of '$old_cardnumber'"; |
| 197 |
return 0; |
| 198 |
} |
| 199 |
} |
| 200 |
} |
| 201 |
} |
| 202 |
|
| 203 |
if ( $newcommer || $config{'update'} ) { |
| 204 |
$debug and logger { "changing attrs for $id" => $$borrower{xattr} }; |
| 205 |
set_xattr $id, $borrower; |
| 206 |
} |
| 207 |
|
| 208 |
return 1 |
| 209 |
} |
| 210 |
|
| 211 |
sub cnx { |
| 212 |
my $cnx = Net::LDAP->new( $ldap->{uri}, onerror => 'die' ) or do { |
| 213 |
warn "ldap error: $!"; |
| 214 |
}; |
| 215 |
# bind MUST success |
| 216 |
my $msg = eval { |
| 217 |
$cnx->bind ($ldap->{manager}, password => $ldap->{password}) |
| 218 |
}; |
| 219 |
debug_msg "ldap $_:", $msg->$_ for qw/ error code /; |
| 220 |
if ( $@ ) { |
| 221 |
return { |
| 222 |
error => 'LDAP_CANTBIND', |
| 223 |
msg => $@ |
| 224 |
}; |
| 225 |
}; |
| 226 |
return $cnx; |
| 227 |
} |
| 228 |
|
| 102 |
sub checkpw_ldap { |
229 |
sub checkpw_ldap { |
| 103 |
my ($dbh, $userid, $password) = @_; |
230 |
my ($dbh, $userid, $password) = @_; |
| 104 |
my @hosts = split(',', $prefhost); |
231 |
my @hosts = split(',', $prefhost); |
|
Lines 109-176
sub checkpw_ldap {
Link Here
|
| 109 |
} |
236 |
} |
| 110 |
|
237 |
|
| 111 |
#$debug and $db->debug(5); |
238 |
#$debug and $db->debug(5); |
|
|
239 |
my $to_borrower = {}; |
| 240 |
|
| 241 |
my $uattr = $$ldap{userid_from} || $$ldap{mapping}{userid}{is} |
| 242 |
or die "userid mapping not set"; |
| 243 |
|
| 112 |
my $userldapentry; |
244 |
my $userldapentry; |
| 113 |
|
245 |
|
| 114 |
if ( $ldap->{auth_by_bind} ) { |
246 |
if ( $$ldap{authmethod} ) { |
| 115 |
# Perform an anonymous bind |
247 |
for ( $$ldap{branch} ) { |
| 116 |
my $res = $db->bind; |
248 |
$_ or die "no branch, no auth"; |
| 117 |
if ( $res->code ) { |
249 |
ref $_ eq 'HASH' and $_ = [$_]; |
| 118 |
$debug and warn "Anonymous LDAP bind failed: ". description($res); |
250 |
} |
| 119 |
return 0; |
|
|
| 120 |
} |
| 121 |
|
251 |
|
| 122 |
# Perform a LDAP search for the given username |
252 |
# This code is an attempt to introduce a new codebase that can be hookable |
| 123 |
my $search = search_method($db, $userid) or return 0; # warnings are in the sub |
253 |
# and can mangage more cases than the old way |
| 124 |
$userldapentry = $search->shift_entry; |
254 |
|
|
|
255 |
# if the filter isn't set, userid mapping is used |
| 256 |
$$ldap{filter} ||= "$uattr=%s"; |
| 257 |
|
| 258 |
my $cnx = cnx or return 0; |
| 259 |
|
| 260 |
# login can be either ... |
| 261 |
my $login = do { |
| 262 |
|
| 263 |
# An Active Directory principal_name. Just replace the %s by the userid |
| 264 |
# well ... don't try if not AD |
| 265 |
if ( $$ldap{authmethod} ~~ [qw/ principal_name principalname principalName /] ) { |
| 266 |
sprintf( $$ldap{principal_name}, $userid ) |
| 267 |
} |
| 268 |
|
| 269 |
# for other LDAP implementation, the standard way is to |
| 270 |
# A) Bind with the manager account and search for the DN of the user entry |
| 271 |
# B) Bind with the user DN and password. |
| 272 |
# Auth is completed if bind success. |
| 273 |
# so in this code; |
| 274 |
# - i fill $userldapentry for later use |
| 275 |
# - i return the DN |
| 276 |
|
| 277 |
elsif ( $$ldap{authmethod} ~~ [qw/ searchdn searchDn search_dn /] ) { |
| 278 |
$to_borrower = _anon_search($cnx, { |
| 279 |
filter => sprintf($$ldap{filter}, $userid) |
| 280 |
}) or do { |
| 281 |
debug_msg "no answer from ldap"; |
| 282 |
return 0; |
| 283 |
}; |
| 284 |
|
| 285 |
if ( $$to_borrower{error} ) { |
| 286 |
say STDERR $$to_borrower{msg}; |
| 287 |
return 0; |
| 288 |
} |
| 125 |
|
289 |
|
| 126 |
# Perform a LDAP bind for the given username using the matched DN |
290 |
$userldapentry = $$to_borrower{entry} or do { |
| 127 |
$res = $db->bind( $userldapentry->dn, password => $password ); |
291 |
debug_msg "no entry returned? weird ..."; |
| 128 |
if ( $res->code ) { |
292 |
}; |
| 129 |
$debug and warn "LDAP bind failed as kohauser $userid: ". description($res); |
293 |
|
| 130 |
return 0; |
294 |
# login is the dn of the entry |
|
|
295 |
if ( $userldapentry ) { $userldapentry->dn } |
| 296 |
else { |
| 297 |
say STDERR "can't authenticate $userid"; |
| 298 |
return 0; |
| 299 |
} |
| 300 |
} else { |
| 301 |
say STDERR "$$ldap{authmethod} authmethod is invalid," |
| 302 |
, "please check your ldap configuration in $ENV{KOHA_CONF}"; |
| 303 |
return 0; |
| 304 |
} |
| 305 |
}; |
| 306 |
|
| 307 |
eval { $cnx->bind( $login, password => $password ) }; |
| 308 |
if ( $@ ) { |
| 309 |
say STDERR "ldap bind with $login failed: $@"; |
| 310 |
return 0; |
| 311 |
} |
| 312 |
debug_msg "congrats, you're one of us"; |
| 313 |
} else { |
| 314 |
# This is the old stuff: |
| 315 |
if ( $ldap->{auth_by_bind} ) { |
| 316 |
# Perform an anonymous bind |
| 317 |
my $res = $db->bind; |
| 318 |
if ( $res->code ) { |
| 319 |
$debug and warn "Anonymous LDAP bind failed: ". description($res); |
| 320 |
return 0; |
| 321 |
} |
| 322 |
|
| 323 |
# Perform a LDAP search for the given username |
| 324 |
my $search = search_method($db, $userid) or return 0; # warnings are in the sub |
| 325 |
$userldapentry = $search->shift_entry; |
| 326 |
|
| 327 |
# Perform a LDAP bind for the given username using the matched DN |
| 328 |
$res = $db->bind( $userldapentry->dn, password => $password ); |
| 329 |
if ( $res->code ) { |
| 330 |
$debug and warn "LDAP bind failed as kohauser $userid: ". description($res); |
| 331 |
return 0; |
| 332 |
} |
| 333 |
} else { |
| 334 |
say STDERR "deprecated kludge: use authmethod search_dn instead"; |
| 335 |
my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword); |
| 336 |
if ($res->code) { # connection refused |
| 337 |
warn "LDAP bind failed as ldapuser " . ($ldapname || '[ANONYMOUS]') . ": " . description($res); |
| 338 |
return 0; |
| 339 |
} |
| 340 |
my $search = search_method($db, $userid) or return 0; # warnings are in the sub |
| 341 |
$userldapentry = $search->shift_entry; |
| 342 |
my $cmpmesg = $db->compare( $userldapentry, attr => 'userpassword', value => $password ); |
| 343 |
if ($cmpmesg->code != 6) { |
| 344 |
warn "LDAP Auth rejected : invalid password for user '$userid'. " . description($cmpmesg); |
| 345 |
return 0; |
| 346 |
} |
| 347 |
} |
| 131 |
} |
348 |
} |
| 132 |
|
349 |
|
| 133 |
} else { |
350 |
if ( my $t = $$ldap{transformation} ) { |
| 134 |
my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword); |
351 |
$$t{subroutine} ||= 'get_borrower'; |
| 135 |
if ($res->code) { # connection refused |
352 |
my $get_borrower = load_subroutine ( @$t{qw/ module subroutine /} ); |
| 136 |
warn "LDAP bind failed as ldapuser " . ($ldapname || '[ANONYMOUS]') . ": " . description($res); |
353 |
unless ( $get_borrower ) { |
| 137 |
return 0; |
354 |
warn "no get_borrower $$t{subroutine} subroutine in $$t{module}"; |
| 138 |
} |
355 |
return 0; |
| 139 |
my $search = search_method($db, $userid) or return 0; # warnings are in the sub |
356 |
} |
| 140 |
$userldapentry = $search->shift_entry; |
357 |
debug_msg "$$t{subroutine} subroutine loaded from $$t{module}"; |
| 141 |
my $cmpmesg = $db->compare( $userldapentry, attr=>'userpassword', value => $password ); |
358 |
if ( my $b = $get_borrower->( $$to_borrower{entry} ) ) { |
| 142 |
if ($cmpmesg->code != 6) { |
359 |
return accept_borrower $b,$userid; |
| 143 |
warn "LDAP Auth rejected : invalid password for user '$userid'. " . description($cmpmesg); |
360 |
} |
| 144 |
return 0; |
361 |
else { return 0 } |
| 145 |
} |
362 |
} |
| 146 |
} |
|
|
| 147 |
|
363 |
|
| 148 |
# To get here, LDAP has accepted our user's login attempt. |
364 |
# To get here, LDAP has accepted our user's login attempt. |
| 149 |
# But we still have work to do. See perldoc below for detailed breakdown. |
365 |
# But we still have work to do. See perldoc below for detailed breakdown. |
| 150 |
|
366 |
|
| 151 |
my (%borrower); |
367 |
my %borrower; |
| 152 |
my ($borrowernumber,$cardnumber,$local_userid,$savedpw) = exists_local($userid); |
368 |
my ($borrowernumber, $cardnumber, $local_userid, $savedpw) = exists_local($userid); |
| 153 |
|
369 |
|
| 154 |
if (( $borrowernumber and $config{update} ) or |
370 |
if ( ( $borrowernumber and $config{update} ) |
| 155 |
(!$borrowernumber and $config{replicate}) ) { |
371 |
or ( !$borrowernumber and $config{replicate} ) ) { |
| 156 |
%borrower = ldap_entry_2_hash($userldapentry,$userid); |
372 |
%borrower = ldap_entry_2_hash( $userldapentry, $userid ); |
| 157 |
$debug and print STDERR "checkpw_ldap received \%borrower w/ " . keys(%borrower), " keys: ", join(' ', keys %borrower), "\n"; |
373 |
$debug and print STDERR "checkpw_ldap received \%borrower w/ " . keys(%borrower), " keys: ", join(' ', keys %borrower), "\n"; |
| 158 |
} |
374 |
} |
| 159 |
|
375 |
|
| 160 |
if ($borrowernumber) { |
376 |
if ($borrowernumber) { |
| 161 |
if ($config{update}) { # A1, B1 |
377 |
if ($config{update}) { # A1, B1 |
| 162 |
my $c2 = &update_local($local_userid,$password,$borrowernumber,\%borrower) || ''; |
378 |
my $c2 = &update_local($local_userid, $password, $borrowernumber, \%borrower) || ''; |
| 163 |
($cardnumber eq $c2) or warn "update_local returned cardnumber '$c2' instead of '$cardnumber'"; |
379 |
($cardnumber eq $c2) or warn "update_local returned cardnumber '$c2' instead of '$cardnumber'"; |
| 164 |
} else { # C1, D1 |
380 |
} else { # C1, D1 |
| 165 |
# maybe update just the password? |
381 |
# maybe update just the password? |
| 166 |
return(1, $cardnumber, $local_userid); |
382 |
return (1, $cardnumber, $local_userid); |
| 167 |
} |
383 |
} |
| 168 |
} elsif ($config{replicate}) { # A2, C2 |
384 |
} elsif ($config{replicate}) { # A2, C2 |
| 169 |
$borrowernumber = AddMember(%borrower) or die "AddMember failed"; |
385 |
$borrowernumber = AddMember(%borrower) or die "AddMember failed"; |
| 170 |
} else { |
386 |
} else { |
| 171 |
return 0; # B2, D2 |
387 |
return 0; # B2, D2 |
| 172 |
} |
388 |
} |
| 173 |
if (C4::Context->preference('ExtendedPatronAttributes') && $borrowernumber && ($config{update} ||$config{replicate})) { |
389 |
if (C4::Context->preference('ExtendedPatronAttributes') && $borrowernumber && ($config{update} || $config{replicate})) { |
| 174 |
my $extended_patron_attributes; |
390 |
my $extended_patron_attributes; |
| 175 |
foreach my $attribute_type ( C4::Members::AttributeTypes::GetAttributeTypes() ) { |
391 |
foreach my $attribute_type ( C4::Members::AttributeTypes::GetAttributeTypes() ) { |
| 176 |
my $code = $attribute_type->{code}; |
392 |
my $code = $attribute_type->{code}; |
|
Lines 178-199
sub checkpw_ldap {
Link Here
|
| 178 |
push @$extended_patron_attributes, { code => $code, value => $borrower{$code} }; |
394 |
push @$extended_patron_attributes, { code => $code, value => $borrower{$code} }; |
| 179 |
} |
395 |
} |
| 180 |
} |
396 |
} |
| 181 |
my @errors; |
397 |
|
| 182 |
#Check before add |
398 |
my @errors; |
| 183 |
for (my $i; $i< scalar(@$extended_patron_attributes)-1;$i++) { |
399 |
#Check before add |
| 184 |
my $attr=$extended_patron_attributes->[$i]; |
400 |
for (my $i; $i < scalar(@$extended_patron_attributes) - 1; $i++) { |
| 185 |
unless (C4::Members::Attributes::CheckUniqueness($attr->{code}, $attr->{value}, $borrowernumber)) { |
401 |
my $attr = $extended_patron_attributes->[$i]; |
| 186 |
unshift @errors, $i; |
402 |
unless (C4::Members::Attributes::CheckUniqueness($attr->{code}, $attr->{value}, $borrowernumber)) { |
| 187 |
warn "ERROR_extended_unique_id_failed $attr->{code} $attr->{value}"; |
403 |
unshift @errors, $i; |
| 188 |
} |
404 |
warn "ERROR_extended_unique_id_failed $attr->{code} $attr->{value}"; |
| 189 |
} |
405 |
} |
| 190 |
#Removing erroneous attributes |
406 |
} |
| 191 |
foreach my $index (@errors){ |
407 |
|
| 192 |
@$extended_patron_attributes=splice(@$extended_patron_attributes,$index,1); |
408 |
#Removing erroneous attributes |
| 193 |
} |
409 |
foreach my $index (@errors) { |
| 194 |
C4::Members::Attributes::SetBorrowerAttributes($borrowernumber, $extended_patron_attributes); |
410 |
@$extended_patron_attributes = splice(@$extended_patron_attributes, $index, 1); |
| 195 |
} |
411 |
} |
| 196 |
return(1, $cardnumber, $userid); |
412 |
C4::Members::Attributes::SetBorrowerAttributes($borrowernumber, $extended_patron_attributes); |
|
|
413 |
} |
| 414 |
return (1, $cardnumber, $userid); |
| 197 |
} |
415 |
} |
| 198 |
|
416 |
|
| 199 |
# Pass LDAP entry object and local cardnumber (userid). |
417 |
# Pass LDAP entry object and local cardnumber (userid). |
|
Lines 202-266
return(1, $cardnumber, $userid);
Link Here
|
| 202 |
# Ensure that mandatory fields are correctly filled! |
420 |
# Ensure that mandatory fields are correctly filled! |
| 203 |
# |
421 |
# |
| 204 |
sub ldap_entry_2_hash { |
422 |
sub ldap_entry_2_hash { |
| 205 |
my $userldapentry = shift; |
423 |
my $userldapentry = shift; |
| 206 |
my %borrower = ( cardnumber => shift ); |
424 |
my %borrower = ( cardnumber => shift ); |
| 207 |
my %memberhash; |
425 |
my %memberhash; |
| 208 |
$userldapentry->exists('uid'); # This is bad, but required! By side-effect, this initializes the attrs hash. |
426 |
$userldapentry->exists('uid'); # This is bad, but required! By side-effect, this initializes the attrs hash. |
| 209 |
if ($debug) { |
427 |
if ($debug) { |
| 210 |
print STDERR "\nkeys(\%\$userldapentry) = " . join(', ', keys %$userldapentry), "\n", $userldapentry->dump(); |
428 |
print STDERR "\nkeys(\%\$userldapentry) = " . join(', ', keys %$userldapentry), "\n", $userldapentry->dump(); |
| 211 |
foreach (keys %$userldapentry) { |
429 |
foreach (keys %$userldapentry) { |
| 212 |
print STDERR "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n"; |
430 |
print STDERR "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n"; |
| 213 |
hashdump("LDAP key: ",$userldapentry->{$_}); |
431 |
hashdump("LDAP key: ",$userldapentry->{$_}); |
| 214 |
} |
432 |
} |
| 215 |
} |
433 |
} |
| 216 |
my $x = $userldapentry->{attrs} or return; |
434 |
my $x = $userldapentry->{attrs} or return; |
| 217 |
foreach (keys %$x) { |
435 |
foreach (keys %$x) { |
| 218 |
$memberhash{$_} = join ' ', @{$x->{$_}}; |
436 |
$memberhash{$_} = join ' ', @{$x->{$_}}; |
| 219 |
$debug and print STDERR sprintf("building \$memberhash{%s} = ", $_, join(' ', @{$x->{$_}})), "\n"; |
437 |
$debug and print STDERR sprintf("building \$memberhash{%s} = ", $_, join(' ', @{$x->{$_}})), "\n"; |
| 220 |
} |
438 |
} |
| 221 |
$debug and print STDERR "Finsihed \%memberhash has ", scalar(keys %memberhash), " keys\n", |
439 |
$debug and print STDERR "Finsihed \%memberhash has ", scalar(keys %memberhash), " keys\n", |
| 222 |
"Referencing \%mapping with ", scalar(keys %mapping), " keys\n"; |
440 |
"Referencing \%mapping with ", scalar(keys %mapping), " keys\n"; |
| 223 |
foreach my $key (keys %mapping) { |
441 |
foreach my $key (keys %mapping) { |
| 224 |
my $data = $memberhash{ lc($mapping{$key}->{is}) }; # Net::LDAP returns all names in lowercase |
442 |
my $data = $memberhash{ lc($mapping{$key}->{is}) }; # Net::LDAP returns all names in lowercase |
| 225 |
$debug and printf STDERR "mapping %20s ==> %-20s (%s)\n", $key, $mapping{$key}->{is}, $data; |
443 |
$debug and printf STDERR "mapping %20s ==> %-20s (%s)\n", $key, $mapping{$key}->{is}, $data; |
| 226 |
unless (defined $data) { |
444 |
unless (defined $data) { |
| 227 |
$data = $mapping{$key}->{content} || ''; # default or failsafe '' |
445 |
$data = $mapping{$key}->{content} || ''; # default or failsafe '' |
| 228 |
} |
446 |
} |
| 229 |
$borrower{$key} = ($data ne '') ? $data : ' ' ; |
447 |
$borrower{$key} = ($data ne '') ? $data : ' '; |
| 230 |
} |
448 |
} |
| 231 |
$borrower{initials} = $memberhash{initials} || |
449 |
$borrower{initials} = $memberhash{initials} || |
| 232 |
( substr($borrower{'firstname'},0,1) |
450 |
( substr($borrower{'firstname'},0,1) |
| 233 |
. substr($borrower{ 'surname' },0,1) |
451 |
. substr($borrower{ 'surname' },0,1) |
| 234 |
. " "); |
452 |
. " "); |
| 235 |
|
453 |
|
| 236 |
# check if categorycode exists, if not, fallback to default from koha-conf.xml |
454 |
# check if categorycode exists, if not, fallback to default from koha-conf.xml |
| 237 |
my $dbh = C4::Context->dbh; |
455 |
my $dbh = C4::Context->dbh; |
| 238 |
my $sth = $dbh->prepare("SELECT categorycode FROM categories WHERE categorycode = ?"); |
456 |
my $sth = $dbh->prepare("SELECT categorycode FROM categories WHERE categorycode = ?"); |
| 239 |
$sth->execute( uc($borrower{'categorycode'}) ); |
457 |
$sth->execute( uc($borrower{'categorycode'}) ); |
| 240 |
unless ( my $row = $sth->fetchrow_hashref ) { |
458 |
unless ( my $row = $sth->fetchrow_hashref ) { |
| 241 |
my $default = $mapping{'categorycode'}->{content}; |
459 |
my $default = $mapping{'categorycode'}->{content}; |
| 242 |
$debug && warn "Can't find ", $borrower{'categorycode'}, " default to: $default for ", $borrower{userid}; |
460 |
$debug && warn "Can't find ", $borrower{'categorycode'}, " default to: $default for ", $borrower{userid}; |
| 243 |
$borrower{'categorycode'} = $default |
461 |
$borrower{'categorycode'} = $default |
| 244 |
} |
462 |
} |
| 245 |
|
463 |
|
| 246 |
return %borrower; |
464 |
return %borrower; |
| 247 |
} |
465 |
} |
| 248 |
|
466 |
|
| 249 |
sub exists_local { |
467 |
sub exists_local { |
| 250 |
my $arg = shift; |
468 |
my $arg = shift; |
| 251 |
my $dbh = C4::Context->dbh; |
469 |
my $dbh = C4::Context->dbh; |
| 252 |
my $select = "SELECT borrowernumber,cardnumber,userid,password FROM borrowers "; |
470 |
my $select = "SELECT borrowers.borrowernumber,cardnumber,userid,borrowers.password FROM borrowers "; |
| 253 |
|
471 |
|
| 254 |
my $sth = $dbh->prepare("$select WHERE userid=?"); # was cardnumber=? |
472 |
my $sth = $dbh->prepare("$select WHERE userid=?"); # was cardnumber=? |
| 255 |
$sth->execute($arg); |
473 |
$sth->execute($arg); |
| 256 |
$debug and printf STDERR "Userid '$arg' exists_local? %s\n", $sth->rows; |
474 |
$debug and printf STDERR "Userid '$arg' exists_local? %s\n", $sth->rows; |
| 257 |
($sth->rows == 1) and return $sth->fetchrow; |
475 |
($sth->rows == 1) and return $sth->fetchrow; |
| 258 |
|
476 |
|
| 259 |
$sth = $dbh->prepare("$select WHERE cardnumber=?"); |
477 |
$sth = $dbh->prepare("$select WHERE cardnumber=?"); |
| 260 |
$sth->execute($arg); |
478 |
$sth->execute($arg); |
| 261 |
$debug and printf STDERR "Cardnumber '$arg' exists_local? %s\n", $sth->rows; |
479 |
$debug and printf STDERR "Cardnumber '$arg' exists_local? %s\n", $sth->rows; |
| 262 |
($sth->rows == 1) and return $sth->fetchrow; |
480 |
($sth->rows == 1) and return $sth->fetchrow; |
| 263 |
return 0; |
481 |
|
|
|
482 |
$sth = $dbh->prepare("$select JOIN borrower_attributes USING (borrowernumber) WHERE attribute=?"); |
| 483 |
$sth->execute($arg); |
| 484 |
$debug and printf STDERR "attribute '$arg' exists_local? %s\n", $sth->rows; |
| 485 |
($sth->rows == 1) and return $sth->fetchrow; |
| 486 |
return 0; |
| 264 |
} |
487 |
} |
| 265 |
|
488 |
|
| 266 |
sub _do_changepassword { |
489 |
sub _do_changepassword { |
|
Lines 268-307
sub _do_changepassword {
Link Here
|
| 268 |
$debug and print STDERR "changing local password for borrowernumber=$borrowerid to '$digest'\n"; |
491 |
$debug and print STDERR "changing local password for borrowernumber=$borrowerid to '$digest'\n"; |
| 269 |
changepassword($userid, $borrowerid, $digest); |
492 |
changepassword($userid, $borrowerid, $digest); |
| 270 |
|
493 |
|
| 271 |
# Confirm changes |
494 |
# Confirm changes |
| 272 |
my $sth = C4::Context->dbh->prepare("SELECT password,cardnumber FROM borrowers WHERE borrowernumber=? "); |
495 |
my $sth = C4::Context->dbh->prepare("SELECT password,cardnumber FROM borrowers WHERE borrowernumber=? "); |
| 273 |
$sth->execute($borrowerid); |
496 |
$sth->execute($borrowerid); |
| 274 |
if ($sth->rows) { |
497 |
if ($sth->rows) { |
| 275 |
my ($md5password, $cardnum) = $sth->fetchrow; |
498 |
my ($md5password, $cardnum) = $sth->fetchrow; |
| 276 |
($digest eq $md5password) and return $cardnum; |
499 |
($digest eq $md5password) and return $cardnum; |
| 277 |
warn "Password mismatch after update to cardnumber=$cardnum (borrowernumber=$borrowerid)"; |
500 |
warn "Password mismatch after update to cardnumber=$cardnum (borrowernumber=$borrowerid)"; |
| 278 |
return; |
501 |
return; |
| 279 |
} |
502 |
} |
| 280 |
die "Unexpected error after password update to userid/borrowernumber: $userid / $borrowerid."; |
503 |
die "Unexpected error after password update to userid/borrowernumber: $userid / $borrowerid."; |
| 281 |
} |
504 |
} |
| 282 |
|
505 |
|
| 283 |
sub update_local { |
506 |
sub update_local { |
| 284 |
my $userid = shift or return; |
507 |
my $userid = shift or return; |
| 285 |
my $digest = md5_base64(shift) or return; |
508 |
my $digest = md5_base64(shift) or return; |
| 286 |
my $borrowerid = shift or return; |
509 |
my $borrowerid = shift or return; |
| 287 |
my $borrower = shift or return; |
510 |
my $borrower = shift or return; |
| 288 |
my @keys = keys %$borrower; |
511 |
my @keys = keys %$borrower; |
| 289 |
my $dbh = C4::Context->dbh; |
512 |
my $dbh = C4::Context->dbh; |
| 290 |
my $query = "UPDATE borrowers\nSET " . |
513 |
my $query = " |
| 291 |
join(',', map {"$_=?"} @keys) . |
514 |
UPDATE borrowers |
| 292 |
"\nWHERE borrowernumber=? "; |
515 |
SET " . join( ',', map { "$_=?" } @keys ) . " |
| 293 |
my $sth = $dbh->prepare($query); |
516 |
WHERE borrowernumber=? |
| 294 |
if ($debug) { |
517 |
"; |
| 295 |
print STDERR $query, "\n", |
518 |
my $sth = $dbh->prepare($query); |
| 296 |
join "\n", map {"$_ = '" . $borrower->{$_} . "'"} @keys; |
519 |
if ($debug) { |
| 297 |
print STDERR "\nuserid = $userid\n"; |
520 |
print STDERR $query, "\n", |
| 298 |
} |
521 |
join "\n", map { "$_ = '" . $borrower->{$_} . "'" } @keys; |
| 299 |
$sth->execute( |
522 |
print STDERR "\nuserid = $userid\n"; |
| 300 |
((map {$borrower->{$_}} @keys), $borrowerid) |
523 |
} |
| 301 |
); |
524 |
$sth->execute( ( ( map { $borrower->{$_} } @keys ), $borrowerid ) ); |
| 302 |
|
525 |
|
| 303 |
# MODIFY PASSWORD/LOGIN |
526 |
# MODIFY PASSWORD/LOGIN |
| 304 |
_do_changepassword($userid, $borrowerid, $digest); |
527 |
_do_changepassword($userid, $borrowerid, $digest); |
| 305 |
} |
528 |
} |
| 306 |
|
529 |
|
| 307 |
1; |
530 |
1; |
|
Lines 329-408
C4::Auth - Authenticates Koha users
Link Here
|
| 329 |
Make sure that ALL required fields are populated by your LDAP database (and mapped in KOHA_CONF). |
552 |
Make sure that ALL required fields are populated by your LDAP database (and mapped in KOHA_CONF). |
| 330 |
What are the required fields? Well, in mysql you can check the database table "borrowers" like this: |
553 |
What are the required fields? Well, in mysql you can check the database table "borrowers" like this: |
| 331 |
|
554 |
|
| 332 |
mysql> show COLUMNS from borrowers; |
555 |
mysql> show COLUMNS from borrowers; |
| 333 |
+---------------------+--------------+------+-----+---------+----------------+ |
556 |
+---------------------+--------------+------+-----+---------+----------------+ |
| 334 |
| Field | Type | Null | Key | Default | Extra | |
557 |
| Field | Type | Null | Key | Default | Extra | |
| 335 |
+---------------------+--------------+------+-----+---------+----------------+ |
558 |
+---------------------+--------------+------+-----+---------+----------------+ |
| 336 |
| borrowernumber | int(11) | NO | PRI | NULL | auto_increment | |
559 |
| borrowernumber | int(11) | NO | PRI | NULL | auto_increment | |
| 337 |
| cardnumber | varchar(16) | YES | UNI | NULL | | |
560 |
| cardnumber | varchar(16) | YES | UNI | NULL | | |
| 338 |
| surname | mediumtext | NO | | NULL | | |
561 |
| surname | mediumtext | NO | | NULL | | |
| 339 |
| firstname | text | YES | | NULL | | |
562 |
| firstname | text | YES | | NULL | | |
| 340 |
| title | mediumtext | YES | | NULL | | |
563 |
| title | mediumtext | YES | | NULL | | |
| 341 |
| othernames | mediumtext | YES | | NULL | | |
564 |
| othernames | mediumtext | YES | | NULL | | |
| 342 |
| initials | text | YES | | NULL | | |
565 |
| initials | text | YES | | NULL | | |
| 343 |
| streetnumber | varchar(10) | YES | | NULL | | |
566 |
| streetnumber | varchar(10) | YES | | NULL | | |
| 344 |
| streettype | varchar(50) | YES | | NULL | | |
567 |
| streettype | varchar(50) | YES | | NULL | | |
| 345 |
| address | mediumtext | NO | | NULL | | |
568 |
| address | mediumtext | NO | | NULL | | |
| 346 |
| address2 | text | YES | | NULL | | |
569 |
| address2 | text | YES | | NULL | | |
| 347 |
| city | mediumtext | NO | | NULL | | |
570 |
| city | mediumtext | NO | | NULL | | |
| 348 |
| state | mediumtext | YES | | NULL | | |
571 |
| state | text | YES | | NULL | | |
| 349 |
| zipcode | varchar(25) | YES | | NULL | | |
572 |
| zipcode | varchar(25) | YES | | NULL | | |
| 350 |
| country | text | YES | | NULL | | |
573 |
| country | text | YES | | NULL | | |
| 351 |
| email | mediumtext | YES | | NULL | | |
574 |
| email | mediumtext | YES | | NULL | | |
| 352 |
| phone | text | YES | | NULL | | |
575 |
| phone | text | YES | | NULL | | |
| 353 |
| mobile | varchar(50) | YES | | NULL | | |
576 |
| mobile | varchar(50) | YES | | NULL | | |
| 354 |
| fax | mediumtext | YES | | NULL | | |
577 |
| fax | mediumtext | YES | | NULL | | |
| 355 |
| emailpro | text | YES | | NULL | | |
578 |
| emailpro | text | YES | | NULL | | |
| 356 |
| phonepro | text | YES | | NULL | | |
579 |
| phonepro | text | YES | | NULL | | |
| 357 |
| B_streetnumber | varchar(10) | YES | | NULL | | |
580 |
| B_streetnumber | varchar(10) | YES | | NULL | | |
| 358 |
| B_streettype | varchar(50) | YES | | NULL | | |
581 |
| B_streettype | varchar(50) | YES | | NULL | | |
| 359 |
| B_address | varchar(100) | YES | | NULL | | |
582 |
| B_address | varchar(100) | YES | | NULL | | |
| 360 |
| B_address2 | text | YES | | NULL | | |
583 |
| B_address2 | text | YES | | NULL | | |
| 361 |
| B_city | mediumtext | YES | | NULL | | |
584 |
| B_city | mediumtext | YES | | NULL | | |
| 362 |
| B_state | mediumtext | YES | | NULL | | |
585 |
| B_state | text | YES | | NULL | | |
| 363 |
| B_zipcode | varchar(25) | YES | | NULL | | |
586 |
| B_zipcode | varchar(25) | YES | | NULL | | |
| 364 |
| B_country | text | YES | | NULL | | |
587 |
| B_country | text | YES | | NULL | | |
| 365 |
| B_email | text | YES | | NULL | | |
588 |
| B_email | text | YES | | NULL | | |
| 366 |
| B_phone | mediumtext | YES | | NULL | | |
589 |
| B_phone | mediumtext | YES | | NULL | | |
| 367 |
| dateofbirth | date | YES | | NULL | | |
590 |
| dateofbirth | date | YES | | NULL | | |
| 368 |
| branchcode | varchar(10) | NO | MUL | | | |
591 |
| branchcode | varchar(10) | NO | MUL | | | |
| 369 |
| categorycode | varchar(10) | NO | MUL | | | |
592 |
| categorycode | varchar(10) | NO | MUL | | | |
| 370 |
| dateenrolled | date | YES | | NULL | | |
593 |
| dateenrolled | date | YES | | NULL | | |
| 371 |
| dateexpiry | date | YES | | NULL | | |
594 |
| dateexpiry | date | YES | | NULL | | |
| 372 |
| gonenoaddress | tinyint(1) | YES | | NULL | | |
595 |
| gonenoaddress | tinyint(1) | YES | | NULL | | |
| 373 |
| lost | tinyint(1) | YES | | NULL | | |
596 |
| lost | tinyint(1) | YES | | NULL | | |
| 374 |
| debarred | date | YES | | NULL | | |
597 |
| debarred | date | YES | | NULL | | |
| 375 |
| debarredcomment | varchar(255) | YES | | NULL | | |
598 |
| debarredcomment | varchar(255) | YES | | NULL | | |
| 376 |
| contactname | mediumtext | YES | | NULL | | |
599 |
| contactname | mediumtext | YES | | NULL | | |
| 377 |
| contactfirstname | text | YES | | NULL | | |
600 |
| contactfirstname | text | YES | | NULL | | |
| 378 |
| contacttitle | text | YES | | NULL | | |
601 |
| contacttitle | text | YES | | NULL | | |
| 379 |
| guarantorid | int(11) | YES | MUL | NULL | | |
602 |
| guarantorid | int(11) | YES | MUL | NULL | | |
| 380 |
| borrowernotes | mediumtext | YES | | NULL | | |
603 |
| borrowernotes | mediumtext | YES | | NULL | | |
| 381 |
| relationship | varchar(100) | YES | | NULL | | |
604 |
| relationship | varchar(100) | YES | | NULL | | |
| 382 |
| ethnicity | varchar(50) | YES | | NULL | | |
605 |
| ethnicity | varchar(50) | YES | | NULL | | |
| 383 |
| ethnotes | varchar(255) | YES | | NULL | | |
606 |
| ethnotes | varchar(255) | YES | | NULL | | |
| 384 |
| sex | varchar(1) | YES | | NULL | | |
607 |
| sex | varchar(1) | YES | | NULL | | |
| 385 |
| password | varchar(30) | YES | | NULL | | |
608 |
| password | varchar(30) | YES | | NULL | | |
| 386 |
| flags | int(11) | YES | | NULL | | |
609 |
| flags | int(11) | YES | | NULL | | |
| 387 |
| userid | varchar(30) | YES | MUL | NULL | | |
610 |
| userid | varchar(75) | YES | MUL | NULL | | |
| 388 |
| opacnote | mediumtext | YES | | NULL | | |
611 |
| opacnote | mediumtext | YES | | NULL | | |
| 389 |
| contactnote | varchar(255) | YES | | NULL | | |
612 |
| contactnote | varchar(255) | YES | | NULL | | |
| 390 |
| sort1 | varchar(80) | YES | | NULL | | |
613 |
| sort1 | varchar(80) | YES | | NULL | | |
| 391 |
| sort2 | varchar(80) | YES | | NULL | | |
614 |
| sort2 | varchar(80) | YES | | NULL | | |
| 392 |
| altcontactfirstname | varchar(255) | YES | | NULL | | |
615 |
| altcontactfirstname | varchar(255) | YES | | NULL | | |
| 393 |
| altcontactsurname | varchar(255) | YES | | NULL | | |
616 |
| altcontactsurname | varchar(255) | YES | | NULL | | |
| 394 |
| altcontactaddress1 | varchar(255) | YES | | NULL | | |
617 |
| altcontactaddress1 | varchar(255) | YES | | NULL | | |
| 395 |
| altcontactaddress2 | varchar(255) | YES | | NULL | | |
618 |
| altcontactaddress2 | varchar(255) | YES | | NULL | | |
| 396 |
| altcontactaddress3 | varchar(255) | YES | | NULL | | |
619 |
| altcontactaddress3 | varchar(255) | YES | | NULL | | |
| 397 |
| altcontactstate | mediumtext | YES | | NULL | | |
620 |
| altcontactstate | text | YES | | NULL | | |
| 398 |
| altcontactzipcode | varchar(50) | YES | | NULL | | |
621 |
| altcontactzipcode | varchar(50) | YES | | NULL | | |
| 399 |
| altcontactcountry | text | YES | | NULL | | |
622 |
| altcontactcountry | text | YES | | NULL | | |
| 400 |
| altcontactphone | varchar(50) | YES | | NULL | | |
623 |
| altcontactphone | varchar(50) | YES | | NULL | | |
| 401 |
| smsalertnumber | varchar(50) | YES | | NULL | | |
624 |
| smsalertnumber | varchar(50) | YES | | NULL | | |
| 402 |
| privacy | int(11) | NO | | 1 | | |
625 |
| privacy | int(11) | NO | | 1 | | |
| 403 |
+---------------------+--------------+------+-----+---------+----------------+ |
626 |
+---------------------+--------------+------+-----+---------+----------------+ |
| 404 |
66 rows in set (0.00 sec) |
627 |
67 rows in set (0.00 sec) |
| 405 |
Where Null="NO", the field is required. |
628 |
Where Null="NO", the field is required. |
| 406 |
|
629 |
|
| 407 |
=head1 KOHA_CONF and field mapping |
630 |
=head1 KOHA_CONF and field mapping |
| 408 |
|
631 |
|