Skip to content

Commit

Permalink
Item12479: preserve existing topic user mapping
Browse files Browse the repository at this point in the history
... while migrating to ldap user mapping
  • Loading branch information
MichaelDaum committed Aug 28, 2014
1 parent 6427a08 commit e2ab7cf
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 37 deletions.
9 changes: 9 additions & 0 deletions .gitignore
@@ -0,0 +1,9 @@
*.gz
*.swp
LdapContrib.md5
LdapContrib.sha1
LdapContrib.tgz
LdapContrib.txt
LdapContrib.zip
LdapContrib_installer
LdapContrib_installer.pl
11 changes: 7 additions & 4 deletions data/System/LdapContrib.txt
@@ -1,5 +1,4 @@
%META:TOPICINFO{author="ProjectContributor" comment="autosave" date="1356278775" format="1.1" version="4"}%

---+!! %TOPIC%
%TOC%
---++ Introduction
Expand Down Expand Up @@ -140,7 +139,6 @@ in the group objects, and may map the member object indirectly to the
login name. In addition any "primary group" setting stored in the user objects
is consulted as well.


---++ Normalization of login, wiki and group names
<nop>LdapContrib reads three kinds of names from your LDAP server and reuses this information
as needed. These are the login names - used to log in -,
Expand All @@ -158,6 +156,10 @@ setting the parameters
* =WikiNameAliases=: a comma separated key=value list of <nop>WikiNames to be mapped
to another <nop>WikiName (DEPCRETATED: use =RewriteWikiNames= instead).
* =RewriteWikiNames=: a list of rewrite rules; see below
* =UserMappingTopic=: a topic that holds a pre-defined login-to-wikiname mapping; this
comes in handy when migrating from a <nop>TopicUserMapping to an LDAP setup where
names already in use should be preserved. Note that no rewrite or normalization is applied
if a login name is found in the database

Given the setting

Expand Down Expand Up @@ -502,12 +504,13 @@ This work was partly sponsored by
* [[http://www.1and1.com][1&1]]

| Author: | Michael Daum |
| Copyright&nbsp;&copy;: | 2006-2014 Michael Daum http://michaeldaumconsulting.com |
| Copyright: | &copy; 2006-2014 Michael Daum http://michaeldaumconsulting.com |
| License: | GPL ([[http://www.gnu.org/copyleft/gpl.html][GNU General Public License]]) |
| Release: | %$RELEASE% |
| Version: | %$VERSION% |
| Change History: | <!-- versions below in reverse order -->&nbsp; |
| 18 Nov 2014: | using proper locking when updating the ldap cache (Foswiki:Main/TerjeNessAndersen); \
| 23 May 2014: | implemented feature to provide a login-to-wikiname mapping in a topic |
| 18 Mar 2014: | using proper locking when updating the ldap cache (Foswiki:Main/TerjeNessAndersen); \
rewrite of name clash resolution to use login attributes, not DNs anymore |
| 29 Nov 2012: | fixed decoding of ldap strings |
| 21 Nov 2012: | fixed ldap login manager using old api; \
Expand Down
95 changes: 82 additions & 13 deletions lib/Foswiki/Contrib/LdapContrib.pm
Expand Up @@ -30,8 +30,8 @@ use Encode ();
use Foswiki::Func ();
use Foswiki::Plugins ();

our $VERSION = '6.00';
our $RELEASE = '6.00';
our $VERSION = '6.10';
our $RELEASE = '6.10';
our %sharedLdapContrib;

=pod
Expand Down Expand Up @@ -178,6 +178,8 @@ sub new {

wikiNameAliases => $Foswiki::cfg{Ldap}{WikiNameAliases} || '',

userMappingTopic => $Foswiki::cfg{Ldap}{UserMappingTopic} || '',

normalizeWikiName => $Foswiki::cfg{Ldap}{NormalizeWikiNames},
normalizeLoginName => $Foswiki::cfg{Ldap}{NormalizeLoginNames},
caseSensitiveLogin => $Foswiki::cfg{Ldap}{CaseSensitiveLogin} || 0,
Expand Down Expand Up @@ -349,6 +351,7 @@ sub connect {
version => $this->{version},
inet4 => ($this->{ipv6}?0:1),
inet6 => ($this->{ipv6}?1:0),
timeout => 5, # TODO: make configurable
);

unless ($this->{ldap}) {
Expand Down Expand Up @@ -391,13 +394,21 @@ sub connect {
# sasl bind
my $sasl = Authen::SASL->new(
mechanism => $this->{saslMechanism}, #'DIGEST-MD5 PLAIN CRAM-MD5 EXTERNAL ANONYMOUS',
callback => {
);

if ($this->{bindDN} && $this->{bindPassword}) {
$sasl->callback(
user => $this->{bindDN},
pass => $this->{bindPassword},
},
);
#writeDebug("sasl bind to $this->{bindDN}");
$msg = $this->{ldap}->bind($this->{bindDN}, sasl => $sasl, version => $this->{version});
);
#writeDebug("sasl bind to $this->{bindDN}");
$msg = $this->{ldap}->bind($this->{bindDN}, sasl => $sasl, version => $this->{version});

} else {
# writeDebug("sasl bind without user or pass");
$msg = $this->{ldap}->bind(sasl => $sasl, version => $this->{version});
}

} else {
# simple bind
#writeDebug("proxy bind");
Expand Down Expand Up @@ -455,6 +466,12 @@ sub finish {
$this->disconnect();
delete $sharedLdapContrib{$this->{session}};

undef $this->{_topicUserMapping};
undef $this->{_wikiNameClaches};
undef $this->{_primaryGroup};
undef $this->{_groups};
undef $this->{_groupId};

$this->untieCache();
}

Expand Down Expand Up @@ -807,8 +824,6 @@ sub initCache {
unless $Foswiki::cfg{UserMappingManager} =~ /LdapUserMapping/
|| $Foswiki::cfg{PasswordManager} =~ /LdapPasswdUser/;

#writeDebug("called initCache");

# open cache
#writeDebug("opening ldap cache from $this->{cacheFile}");
$this->tieCache('read');
Expand Down Expand Up @@ -845,6 +860,46 @@ sub initCache {

=pod
---++ initTopicUserMapping()
reads a topic-based user mapping from a predefined topic and initializes the internal _topicUserMapping hash
=cut

sub initTopicUserMapping {
my $this = shift;

return unless $this->{userMappingTopic};

my ($web, $topic) = Foswiki::Func::normalizeWebTopicName(undef, $this->{userMappingTopic});

unless (Foswiki::Func::topicExists($web, $topic)) {
writeDebug("UserMappingTopic $web.$topic not found");
return;
}

writeDebug("reading topic mapping from $web.$topic");

my ($meta, $text) = Foswiki::Func::readTopic($web, $topic);

$this->{_topicUserMapping} = ();

while ($text =~ /^\s*\* (?:$Foswiki::regex{webNameRegex}\.)?($Foswiki::regex{wikiWordRegex})\s*(?:-\s*(\S+)\s*)?-.*$/gm) {
my $wikiName = $1;
my $loginName = $2 || $wikiName;

# can't use basemapping obj here as it doesn't exist yet
# next if $this->{session}->{users}->{basemapping}->handlesUser(undef, $loginName, $wikiName);

next if $wikiName =~ /^(ProjectContributor|$Foswiki::cfg{Register}{RegistrationAgentWikiName}|$Foswiki::cfg{AdminUserWikiName}|$Foswiki::cfg{DefaultUserWikiName}|UnknownUser)$/;

writeDebug("topic mapping $wikiName - $loginName");
$this->{_topicUserMapping}{$loginName} = $wikiName;
}
}

=pod
---++ refreshCache($mode) -> $boolean
download all relevant records from the LDAP server and
Expand All @@ -861,8 +916,6 @@ sub refreshCache {

return unless $mode;

writeDebug("called refreshCache(mode=$mode)");

$this->{_refreshMode} = $mode;

# create a temporary tie
Expand Down Expand Up @@ -950,6 +1003,9 @@ sub refreshUsersCache {
attrs => [$this->{loginAttribute}, $this->{mailAttribute}, $this->{primaryGroupAttribute}, @{$this->{wikiNameAttributes}}],
);

# init the topic mapping if required
$this->initTopicUserMapping();

# use the control LDAP extension only if a valid pageSize value has been provided
my $page;
my $cookie;
Expand Down Expand Up @@ -1032,6 +1088,14 @@ sub refreshUsersCache {

writeDebug("got $nrRecords keys in cache");

if ($this->{_topicUserMapping}) {
foreach my $wikiName (sort values %{$this->{_topicUserMapping}}) {
next if $wikiNames{$wikiName};
#print STDERR " * %USERSWEB%.$wikiName not found in LDAP\n";
writeDebug("$wikiName not found in LDAP ");
}
}

return 1;
}

Expand Down Expand Up @@ -1261,8 +1325,13 @@ sub cacheUserFromEntry {
return 0 if $this->{excludeMap}{$loginName};

# construct the wikiName
my $isExplicitWikiName = (defined $wikiName) ? 1 : 0;
if ($isExplicitWikiName) {
if (!defined $wikiName && defined $this->{_topicUserMapping}) {
$wikiName = $this->{_topicUserMapping}{$loginName};
writeDebug("found wikiName for $loginName in topic mapping: $wikiName ... not reading ldap attributes")
if defined $wikiName;
}

if (defined $wikiName) {
#writeDebug("found explicit wikiName '$wikiName' for $dn");
} else {

Expand Down
7 changes: 7 additions & 0 deletions lib/Foswiki/Contrib/LdapContrib/Config.spec
Expand Up @@ -178,6 +178,13 @@ $Foswiki::cfg{Ldap}{AllowChangePassword} = 0;
# registered to the wiki natively. Note, that <b>this must not be Foswiki::Users::LdapPasswdUser again!</b>
$Foswiki::cfg{Ldap}{SecondaryPasswordManager} = 'none';

# **STRING**
# This parameter allows to hard-code a LoginName-to-WikiName mapping in a wiki topic. This
# feature may be used to migrate from a TopicUserMapping to LdapUserMapping by preserving any already existing
# mapping stored in Main.WikiUsers. Leave it empty to disable this feature and build WikiNames by reading
# LDAP attributes as normal.
$Foswiki::cfg{Ldap}{UserMappingTopic} = '';

# ---+++ Group settings
# The settings below configures the mapping and processing of LoginNames and WikiNames as
# well as the use of LDAP groups.
Expand Down
2 changes: 1 addition & 1 deletion lib/Foswiki/Contrib/LdapContrib/DEPENDENCIES
@@ -1,6 +1,6 @@
Authen::SASL,>=2.00,cpan,Optional
DB_File,>=1.00,cpan,Required
DB_File::Lock,>=1.00,cpan,Required
DB_File::Lock,>=0.05,cpan,Required
Digest::MD5,>=2.36,cpan,Required
Net::LDAP,>=0.33,cpan,Required
IO::Socket::SSL,>=1.0,cpan,Optional
Expand Down
50 changes: 31 additions & 19 deletions lib/Foswiki/Users/LdapUserMapping.pm
Expand Up @@ -18,6 +18,7 @@
package Foswiki::Users::LdapUserMapping;

use strict;
use warnings;
use Foswiki::Contrib::LdapContrib ();
use Foswiki::ListIterator ();

Expand Down Expand Up @@ -120,11 +121,7 @@ sub getLoginName {
# Remove the mapping id in case this is a subclass
$login =~ s/$this->{mapping_id}// if $this->{mapping_id};

use bytes;
# Reverse the encoding used to generate cUIDs in login2cUID
# use bytes to ignore character encoding
$login =~ s/_([0-9a-f][0-9a-f])/chr(hex($1))/gei;
no bytes;
$login = _mapcUID2Login($login);

$login = lc($login) unless $this->{ldap}{caseSensitiveLogin};

Expand All @@ -133,6 +130,19 @@ sub getLoginName {
return $login;
}

# Reverse the encoding used to generate cUIDs in login2cUID
# use bytes to ignore character encoding
sub _mapcUID2Login {
my $login = shift;

use bytes;
$login =~ s/_([0-9a-f][0-9a-f])/chr(hex($1))/gei;
no bytes;

return $login;
}


=pod
---++ getWikiName ($cUID) -> wikiname
Expand All @@ -146,8 +156,7 @@ sub getWikiName {

#writeDebug("called LdapUserMapping::getWikiName($cUID)");

my $loginName = $this->getLoginName($cUID);
return undef unless $loginName;
my $loginName = _mapcUID2Login($cUID);

return $loginName if $this->isGroup($loginName);

Expand Down Expand Up @@ -363,8 +372,12 @@ sub eachGroupMember {
push @$result, $it->next;
}
} else {
my $cUID = $this->login2cUID($login);
push @$result, $cUID if $cUID;
if ($this->isGroup($login)) {
push @$result, $login;
} else {
my $cUID = $this->login2cUID($login);
push @$result, $cUID if $cUID;
}
}
}
}
Expand Down Expand Up @@ -416,8 +429,7 @@ sub isGroup {
return 0 unless $user;
#writeDebug("called isGroup($user)");

# may be called using a user object or a wikiName of a user
my $wikiName = (ref $user) ? $user->wikiName : $user;
my $wikiName = _mapcUID2Login($user);

# special treatment for build-in groups
return 1 if $wikiName eq $Foswiki::cfg{SuperAdminGroup};
Expand All @@ -431,7 +443,6 @@ sub isGroup {

# backoff if it does not know
if (!defined($isGroup) && $this->{ldap}{nativeGroupsBackoff}) {
$isGroup = $this->SUPER::isGroup($user) if ref $user;
$isGroup = ($wikiName =~ /Group$/);
}

Expand Down Expand Up @@ -498,6 +509,12 @@ first, then login, then wikiName.
sub handlesUser {
my ($this, $cUID, $login, $wikiName) = @_;

return 0 if $login && $login =~ /^baseusermapping/i;
return 0 if $cUID && $cUID =~ /^baseusermapping/i;
return 0 if $wikiName && $wikiName =~ /^baseusermapping/i;
return 0
if $this->{session}->{users}->{basemapping}->handlesUser($cUID, $login, $wikiName);

if ($this->{ldap}{mapGroups}) {
# ask LDAP
return 1 if $login && $this->{ldap}->isGroup($login);
Expand Down Expand Up @@ -539,16 +556,11 @@ sub login2cUID {
my $loginName = $this->{ldap}->getLoginOfWikiName($name);
$name = $loginName if defined $loginName; # called with a wikiname

$name = lc($name) unless $this->{ldap}{caseSensitiveLogin};
#$name = lc($name) unless $this->{ldap}{caseSensitiveLogin};
my $cUID = $this->{mapping_id} . Foswiki::Users::mapLogin2cUID($name);

unless ($dontcheck) {
my $wikiName = $this->{ldap}->getWikiNameOfLogin($name);
return unless $wikiName || $loginName;
}

# don't ask topic user mapping for large wikis
if ($this->{ldap}{secondaryPasswordManager} && ! defined($cUID)) {
if ($this->{ldap}{secondaryPasswordManager} && (! defined($cUID) || $cUID eq $origName)) {
$cUID = $this->SUPER::login2cUID($origName, $dontcheck);
}

Expand Down

0 comments on commit e2ab7cf

Please sign in to comment.