aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFathi Boudra <fathi.boudra@linaro.org>2014-06-11 17:50:30 +0300
committerFathi Boudra <fathi.boudra@linaro.org>2014-06-11 17:50:30 +0300
commitcc1a2595b753f882b1fe43490d8e4b054086d409 (patch)
treeed69f7a9e5a23eada5a6d31757a01f302a207f34
parentd0970714907f002260a47c78e2dd706d90a22371 (diff)
downloadbugs.linaro.org-cc1a2595b753f882b1fe43490d8e4b054086d409.tar.gz
Add LDAPGroups and LDAPUsers extensions
Signed-off-by: Fathi Boudra <fathi.boudra@linaro.org>
-rw-r--r--extensions/LDAPGroups/Config.pm21
-rw-r--r--extensions/LDAPGroups/Extension.pm212
-rw-r--r--extensions/LDAPGroups/README.md40
-rw-r--r--extensions/LDAPGroups/TODO4
-rw-r--r--extensions/LDAPGroups/lib/Auth/Verify/LDAP.pm48
-rw-r--r--extensions/LDAPGroups/lib/Util.pm104
-rwxr-xr-xextensions/LDAPGroups/sync_LDAPGroups.pl50
-rw-r--r--extensions/LDAPGroups/template/en/default/hook/admin/groups/create-field.html.tmpl16
-rw-r--r--extensions/LDAPGroups/template/en/default/hook/admin/groups/edit-field.html.tmpl15
-rw-r--r--extensions/LDAPGroups/template/en/default/hook/global/user-error-errors.html.tmpl15
-rw-r--r--extensions/LDAPUsers/Config.pm21
-rw-r--r--extensions/LDAPUsers/Extension.pm164
-rw-r--r--extensions/LDAPUsers/README.md4
13 files changed, 714 insertions, 0 deletions
diff --git a/extensions/LDAPGroups/Config.pm b/extensions/LDAPGroups/Config.pm
new file mode 100644
index 0000000..225bb67
--- /dev/null
+++ b/extensions/LDAPGroups/Config.pm
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::LDAPGroups;
+
+use 5.10.1;
+use strict;
+
+use constant NAME => 'LDAPGroups';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME; \ No newline at end of file
diff --git a/extensions/LDAPGroups/Extension.pm b/extensions/LDAPGroups/Extension.pm
new file mode 100644
index 0000000..d9c82ac
--- /dev/null
+++ b/extensions/LDAPGroups/Extension.pm
@@ -0,0 +1,212 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::LDAPGroups;
+
+use 5.10.1;
+use strict;
+use parent qw(Bugzilla::Extension);
+
+use Bugzilla::Error qw(ThrowUserError ThrowCodeError);
+use Bugzilla::Util qw(diff_arrays trim clean_text);
+
+use Bugzilla::Extension::LDAPGroups::Util qw(sync_ldap bind_ldap_for_search);
+
+use Scalar::Util qw(blessed);
+
+use constant GRANT_LDAP => 3;
+
+our $VERSION = '0.01';
+
+
+BEGIN {
+ no warnings 'redefine';
+ *Bugzilla::ldap = \&_bugzilla_ldap;
+ *Bugzilla::Auth::Verify::_orig_create_or_update_user
+ = \&Bugzilla::Auth::Verify::create_or_update_user;
+ *Bugzilla::Auth::Verify::create_or_update_user = \&_create_or_update_user;
+ *Bugzilla::Group::_orig_update = \&Bugzilla::Group::update;
+ *Bugzilla::Group::update = \&_group_update;
+ *Bugzilla::Group::_orig_create = \&Bugzilla::Group::create;
+ *Bugzilla::Group::create = \&_group_create;
+ *Bugzilla::Group::ldap_dn = sub { $_[0]->{ldap_dn}; }
+};
+
+# From Bugzilla::Auth::Verify::LDAP
+sub _bugzilla_ldap {
+ my $class = shift;
+
+ return $class->request_cache->{ldap}
+ if defined $class->request_cache->{ldap};
+
+ my @servers = split(/[\s,]+/, Bugzilla->params->{"LDAPserver"});
+ ThrowCodeError("ldap_server_not_defined") unless @servers;
+
+ require Net::LDAP;
+ my $ldap;
+ foreach (@servers) {
+ $ldap = new Net::LDAP(trim($_));
+ last if $ldap;
+ }
+ ThrowCodeError("ldap_connect_failed",
+ { server => join(", ", @servers) }) unless $ldap;
+
+ # try to start TLS if needed
+ if (Bugzilla->params->{"LDAPstarttls"}) {
+ my $mesg = $ldap->start_tls();
+ ThrowCodeError("ldap_start_tls_failed", { error => $mesg->error() })
+ if $mesg->code();
+ }
+ $class->request_cache->{ldap} = $ldap;
+
+ return $class->request_cache->{ldap};
+}
+
+sub _create_or_update_user {
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $result = $self->_orig_create_or_update_user($params);
+
+ if (exists $params->{ldap_group_dns}) {
+
+ my $sth_add_mapping = $dbh->prepare(
+ qq{INSERT INTO user_group_map
+ (user_id, group_id, isbless, grant_type)
+ VALUES (?, ?, ?, ?)});
+
+ my $sth_remove_mapping = $dbh->prepare(
+ qq{DELETE FROM user_group_map
+ WHERE user_id = ? AND group_id = ?});
+
+ my $user = $result->{user};
+ my @ldap_group_dns = @{ $params->{ldap_group_dns} || [] };
+ my $qmarks = join(',', ('?') x @ldap_group_dns);
+ my $group_ids = $dbh->selectcol_arrayref(
+ "SELECT id FROM groups WHERE ldap_dn IN ($qmarks)", undef,
+ @ldap_group_dns);
+
+ my @user_group_ids;
+ foreach my $group (@{ $user->groups || [] }) {
+ push @user_group_ids, $group->id if defined $group->ldap_dn;
+ }
+
+ my ($removed, $added) = diff_arrays(\@user_group_ids, \@$group_ids);
+
+ $sth_add_mapping->execute($user->id, $_, 0, GRANT_LDAP)
+ foreach @{ $added || [] };
+
+ $sth_remove_mapping->execute($user->id, $_)
+ foreach @{ $removed || [] };
+ }
+
+ return $result;
+}
+
+sub _group_update {
+ my ($self, $params) = @_;
+ $self->set('ldap_dn', Bugzilla->input_params->{ldap_dn});
+ return $self->_orig_update($params);
+}
+
+sub _group_create {
+ my ($class, $params) = @_;
+ $params->{ldap_dn} = scalar Bugzilla->input_params->{ldap_dn};
+ return $class->_orig_create($params);
+}
+
+sub install_update_db {
+ my ($self, $args) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_add_column('groups', 'ldap_dn',
+ { TYPE => 'MEDIUMTEXT', DEFAULT => "''" });
+}
+
+sub auth_verify_methods {
+ my ($self, $args) = @_;
+ my $modules = $args->{modules};
+ if (exists $modules->{LDAP}) {
+ $modules->{LDAP} =
+ 'Bugzilla/Extension/LDAPGroups/Auth/Verify/LDAP.pm';
+ }
+}
+
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my ($object, $columns) = @$args{qw(object columns)};
+
+ if ($object->isa('Bugzilla::Group')) {
+ push (@$columns, 'ldap_dn');
+ }
+}
+
+sub object_columns {
+ my ($self, $args) = @_;
+
+ my ($class, $columns) = @$args{qw(class columns)};
+
+ if ($class->isa('Bugzilla::Group')) {
+ push @$columns, qw(ldap_dn);
+ }
+}
+
+sub object_validators {
+ my ($self, $args) = @_;
+ my ($class, $validators) = @$args{qw(class validators)};
+
+ if ($class->isa('Bugzilla::Group')) {
+ $validators->{ldap_dn} = \&_check_ldap_dn;
+ }
+}
+
+sub _check_ldap_dn {
+ my ($invocant, $ldap_dn, undef, $params) = @_;
+ my $ldap = Bugzilla->ldap;
+
+ bind_ldap_for_search();
+
+ $ldap_dn = clean_text($ldap_dn);
+
+ # LDAP DN is optional, but we must validate it if it was
+ # passed.
+ return if !$ldap_dn;
+
+ # We just want to check if the dn is valid.
+ # 'filter' can't be empty neither omitted.
+ my $dn_result = $ldap->search(( base => $ldap_dn,
+ scope => 'sub',
+ filter => '1=1' ));
+ if ($dn_result->code) {
+ ThrowUserError('group_ldap_dn_invalid', { ldap_dn => $ldap_dn });
+ }
+
+ # Group LDAP DN already in use.
+ my ($group) = @{ Bugzilla::Group->match({ ldap_dn => $ldap_dn }) };
+ my $group_id = blessed($invocant) ? $invocant->id : 0;
+ if (defined $group and $group->id != $group_id) {
+ ThrowUserError('group_ldap_dn_already_in_use',
+ { ldap_dn => $ldap_dn });
+ }
+
+ return $ldap_dn;
+}
+
+sub group_end_of_create {
+ my ($self, $args) = @_;
+ my $group = $args->{'group'};
+}
+
+sub group_end_of_update {
+ my ($self, $args) = @_;
+ my ($group, $changes) = @$args{qw(group changes)};
+ sync_ldap($group) if $group->ldap_dn;
+}
+
+
+
+__PACKAGE__->NAME;
diff --git a/extensions/LDAPGroups/README.md b/extensions/LDAPGroups/README.md
new file mode 100644
index 0000000..b15b397
--- /dev/null
+++ b/extensions/LDAPGroups/README.md
@@ -0,0 +1,40 @@
+LDAPGroups
+==========
+
+Bugzilla extension to map Bugzilla and LDAP groups.
+
+## Installation
+
+1. Copy the LDAPGroups/ to the Bugzilla extensions directory and run the checksetup.pl script.
+2. You should be using 'LDAP' as one of the user_verify_class options.
+3. Create or modify an existent group and add the 'LDAP DN' for it. This will be used to map the Bugzilla Group and the LDAP Group. It will add Bugzilla users to the group if they are also member of the related LDAP group. This will happen automatically when a Bugzilla group is created or modified.
+4. The groups membership will be synced up everytime the user logs in.
+
+## Important notes
+
+The extension does not verify if the user is indirect member of a certain group. That means,
+the user will be added as member only for those groups he is directly member of.
+
+The extension has been tested using OpenLDAP with the virtual attribute 'memberOf' enabled. You have to customize the code if it does not reflect your LDIF.
+
+Example:
+
+ > ldapsearch -x uid=jsmith memberof
+ # extended LDIF
+ #
+ # LDAPv3
+ # base <dc=ldap,dc=bugzilla> (default) with scope subtree
+ # filter: uid=jsmith
+ # requesting: memberof
+ #
+
+ # John Smith, Users, ldap.bugzilla
+ dn: cn=John Smith,ou=Users,dc=ldap,dc=bugzilla
+ memberOf: cn=group2,ou=Groups,dc=ldap,dc=bugzilla
+
+ # search result
+ search: 2
+ result: 0 Success
+
+ # numResponses: 2
+ # numEntries: 1
diff --git a/extensions/LDAPGroups/TODO b/extensions/LDAPGroups/TODO
new file mode 100644
index 0000000..80ed99f
--- /dev/null
+++ b/extensions/LDAPGroups/TODO
@@ -0,0 +1,4 @@
+ - Add documentation for the 'LDAP DN' field in the create group page (Bug 842045)
+ - Add updated message for LDAP DN changes (Bug 842068)
+ - Add documentation to the edit user page explaining how LDAP groups will be marked (Bug 842159)
+ - Implement a script (run in a cron job) to sync up groups membership.
diff --git a/extensions/LDAPGroups/lib/Auth/Verify/LDAP.pm b/extensions/LDAPGroups/lib/Auth/Verify/LDAP.pm
new file mode 100644
index 0000000..4cbc072
--- /dev/null
+++ b/extensions/LDAPGroups/lib/Auth/Verify/LDAP.pm
@@ -0,0 +1,48 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::LDAPGroups::Auth::Verify::LDAP;
+use strict;
+use parent qw(Bugzilla::Auth::Verify::LDAP);
+
+use Bugzilla::Error qw(ThrowCodeError);
+
+use Net::LDAP::Util qw(escape_filter_value);
+
+
+sub check_credentials {
+ my ($self, $params) = @_;
+ $params = $self->SUPER::check_credentials($params);
+ return $params if $params->{failure};
+ my $ldap_group_dns = $self->_ldap_member_of_groups($params->{bz_username});
+ $params->{ldap_group_dns} = $ldap_group_dns if scalar @$ldap_group_dns;
+ return $params;
+}
+
+sub _ldap_member_of_groups {
+ my ($self, $uid) = @_;
+
+ $uid = escape_filter_value($uid);
+ my $uid_attr = Bugzilla->params->{"LDAPuidattribute"};
+ my $base_dn = Bugzilla->params->{"LDAPBaseDN"};
+ my $dn_result = $self->ldap->search(( base => $base_dn,
+ scope => 'sub',
+ filter => "$uid_attr=$uid" ),
+ attrs => ['memberof']);
+
+ if ($dn_result->code) {
+ ThrowCodeError('ldap_search_error',
+ { errstr => $dn_result->error, username => $uid });
+ }
+
+ my @ldap_group_dns;
+ push @ldap_group_dns, $_->get_value('memberof') for $dn_result->entries;
+
+ return \@ldap_group_dns;
+}
+
+1;
diff --git a/extensions/LDAPGroups/lib/Util.pm b/extensions/LDAPGroups/lib/Util.pm
new file mode 100644
index 0000000..c027b1c
--- /dev/null
+++ b/extensions/LDAPGroups/lib/Util.pm
@@ -0,0 +1,104 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::LDAPGroups::Util;
+
+use strict;
+use base qw(Exporter);
+our @EXPORT = qw(
+ sync_ldap
+ bind_ldap_for_search
+);
+
+use Bugzilla;
+use Bugzilla::Error;
+
+sub bind_ldap_for_search {
+ my $ldap = Bugzilla->ldap;
+ my $bind_result;
+ if (Bugzilla->params->{"LDAPbinddn"}) {
+ my ($LDAPbinddn,$LDAPbindpass) =
+ split(":",Bugzilla->params->{"LDAPbinddn"});
+ $bind_result =
+ $ldap->bind($LDAPbinddn, password => $LDAPbindpass);
+ }
+ else {
+ $bind_result = $ldap->bind();
+ }
+ ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error})
+ if $bind_result->code;
+}
+
+sub sync_ldap {
+ my ($group) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $ldap = Bugzilla->ldap;
+
+ bind_ldap_for_search();
+
+ my $sth_add = $dbh->prepare("INSERT INTO user_group_map
+ (user_id, group_id, grant_type, isbless)
+ VALUES (?, ?, ?, 0)");
+
+ my $sth_del = $dbh->prepare("DELETE FROM user_group_map
+ WHERE user_id = ? AND group_id = ?
+ AND grant_type = ? and isbless = 0");
+
+ my $mail_attr = Bugzilla->params->{"LDAPmailattribute"};
+ my $base_dn = Bugzilla->params->{"LDAPBaseDN"};
+
+ # Search for members of the LDAP group.
+ my $filter = "memberof=" . $group->ldap_dn;
+ my @attrs = ($mail_attr);
+ my $dn_result = $ldap->search(( base => $base_dn,
+ scope => 'sub',
+ filter => $filter ), attrs => \@attrs);
+ if ($dn_result->code) {
+ ThrowCodeError('ldap_search_error',
+ { errstr => $dn_result->error, username => $group->name });
+ }
+
+ my @group_members;
+ push @group_members, $_->get_value('mail') foreach $dn_result->entries;
+
+ my $users = Bugzilla->dbh->selectall_hashref(
+ "SELECT userid, group_id, login_name
+ FROM profiles
+ LEFT JOIN user_group_map
+ ON user_group_map.user_id = profiles.userid
+ AND group_id = ?
+ AND grant_type = ?
+ AND isbless = 0
+ WHERE extern_id IS NOT NULL",
+ 'userid', undef, ($group->id, Bugzilla::Extension::LDAPGroups->GRANT_LDAP));
+
+ my @added;
+ my @removed;
+ foreach my $user (values %$users) {
+ # User is no longer member of the group.
+ if (defined $user->{group_id}
+ and !grep { $_ eq $user->{login_name} } @group_members)
+ {
+ push @removed, $user->{userid};
+ }
+
+ # User has been added to the group.
+ if (!defined $user->{group_id}
+ and grep { $_ eq $user->{login_name} } @group_members)
+ {
+
+ push @added, $user->{userid};
+ }
+ }
+
+ $sth_add->execute($_, $group->id, Bugzilla::Extension::LDAPGroups->GRANT_LDAP) foreach @added;
+ $sth_del->execute($_, $group->id, Bugzilla::Extension::LDAPGroups->GRANT_LDAP) foreach @removed;
+
+ return { added => \@added, removed => \@removed };
+}
+
+1;
diff --git a/extensions/LDAPGroups/sync_LDAPGroups.pl b/extensions/LDAPGroups/sync_LDAPGroups.pl
new file mode 100755
index 0000000..ef736d1
--- /dev/null
+++ b/extensions/LDAPGroups/sync_LDAPGroups.pl
@@ -0,0 +1,50 @@
+#!/usr/bin/perl -w
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+#
+# Script to syncronize group members with LDAP on an ad-hoc basis
+#
+
+use strict;
+use warnings;
+use lib qw(. lib);
+
+use Bugzilla;
+BEGIN { Bugzilla->extensions }
+
+use Bugzilla::Extension::LDAPGroups::Util qw(sync_ldap);
+
+# Get all groups where the ldap_dn has been set
+sub get_groups_using_ldap_dn(){
+ my @groups = Bugzilla::Group->get_all;
+
+ my @groups_with_ldap_dn;
+
+ foreach my $group (@groups){
+ if ($group->ldap_dn){
+ push @groups_with_ldap_dn, $group;
+ }
+ }
+
+ return @groups_with_ldap_dn;
+}
+
+sub main(){
+ my @groups = get_groups_using_ldap_dn();
+
+ # For every group that has a ldap_dn update
+ # the groups' members according to LDAP
+ foreach my $group (@groups){
+ sync_ldap($group);
+ }
+
+ return;
+}
+
+main();
diff --git a/extensions/LDAPGroups/template/en/default/hook/admin/groups/create-field.html.tmpl b/extensions/LDAPGroups/template/en/default/hook/admin/groups/create-field.html.tmpl
new file mode 100644
index 0000000..7d3d077
--- /dev/null
+++ b/extensions/LDAPGroups/template/en/default/hook/admin/groups/create-field.html.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF Bugzilla.params.user_verify_class.match('LDAP') %]
+ <tr>
+ <th>LDAP DN:</th>
+ <td colspan="3">
+ <input type="text" size="70" maxlength="255" id="ldap_dn" name="ldap_dn">
+ </td>
+ </tr>
+[% END %]
diff --git a/extensions/LDAPGroups/template/en/default/hook/admin/groups/edit-field.html.tmpl b/extensions/LDAPGroups/template/en/default/hook/admin/groups/edit-field.html.tmpl
new file mode 100644
index 0000000..74322ec
--- /dev/null
+++ b/extensions/LDAPGroups/template/en/default/hook/admin/groups/edit-field.html.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF Bugzilla.params.user_verify_class.match('LDAP') %]
+ <tr>
+ <th>LDAP DN:</th>
+ <td><input type="text" name="ldap_dn" size="70" maxlength="255"
+ value="[% group.ldap_dn FILTER html %]"></td>
+ </tr>
+[% END %]
diff --git a/extensions/LDAPGroups/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/LDAPGroups/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 0000000..92db324
--- /dev/null
+++ b/extensions/LDAPGroups/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "group_ldap_dn_already_in_use" %]
+ [% title == "Group LDAP DN already in use" %]
+ The LDAP DN '[% ldap_dn FILTER html %]' is already in use.
+[% ELSIF error == "group_ldap_dn_invalid" %]
+ [% title == "Group LDAP DN invalid" %]
+ The LDAP DN '[% ldap_dn FILTER html %]' is invalid.
+[% END %]
diff --git a/extensions/LDAPUsers/Config.pm b/extensions/LDAPUsers/Config.pm
new file mode 100644
index 0000000..4d051c1
--- /dev/null
+++ b/extensions/LDAPUsers/Config.pm
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::LDAPUsers;
+
+use 5.10.1;
+use strict;
+
+use constant NAME => 'LDAPUsers';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME; \ No newline at end of file
diff --git a/extensions/LDAPUsers/Extension.pm b/extensions/LDAPUsers/Extension.pm
new file mode 100644
index 0000000..c39b10d
--- /dev/null
+++ b/extensions/LDAPUsers/Extension.pm
@@ -0,0 +1,164 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::LDAPUsers;
+
+use 5.10.1;
+use strict;
+use parent qw(Bugzilla::Extension);
+
+our $VERSION = '0.01';
+
+use Bugzilla::Util qw(generate_random_password
+ trick_taint clean_text);
+
+use Net::LDAP::Util qw(escape_filter_value);
+use Storable qw(dclone);
+
+
+BEGIN {
+ no warnings 'redefine';
+ *Bugzilla::has_ldap_enabled = \&_bugzilla_has_ldap_enabled;
+ *Bugzilla::User::_ldapusers_orig_match_field
+ = \&Bugzilla::User::match_field;
+ *Bugzilla::User::match_field = \&_ldapusers_user_match_field;
+};
+
+sub _bugzilla_has_ldap_enabled {
+ my $class = shift;
+ return Bugzilla->params->{user_verify_class} =~ /LDAP/ ? 1 : 0;
+}
+
+sub _ldapusers_user_match_field {
+ my ($fields, $data, $behavior) = @_;
+
+ # That means we are attaching something and a FileHandler is open
+ # and it is not serializable by dclone.
+ my $input_params = Bugzilla->input_params;
+ if (!exists Bugzilla->input_params->{'contenttypeselection'}
+ and !exists Bugzilla->input_params->{'ispatch'})
+ {
+ # We backup input_params because match_fields deletes
+ # some fields.
+ $input_params = dclone(Bugzilla->input_params);
+ }
+
+ # We first try to match existent Bugzilla users.
+ my ($retval, $non_conclusive_fields)
+ = Bugzilla::User::_ldapusers_orig_match_field(
+ $fields, $data, Bugzilla::User->MATCH_SKIP_CONFIRM);
+
+ #XXX
+ foreach my $field (@{ $non_conclusive_fields || [] }) {
+ Bugzilla->input_params->{$field} = $input_params->{$field};
+ }
+
+ # Then we try LDAP users.
+ if ($retval == Bugzilla::User->USER_MATCH_FAILED
+ and Bugzilla->has_ldap_enabled)
+ {
+ _search_for_ldap_user_and_create($non_conclusive_fields, $data);
+ }
+
+ # And finally, we call match_field again after possibly adding
+ # new LDAP users.
+ return Bugzilla::User::_ldapusers_orig_match_field(@_);
+}
+
+sub _search_for_ldap_user_and_create {
+ my ($fields, $data) = @_;
+ $data ||= Bugzilla->input_params;
+
+ # Based on Bugzilla::User::match_field code
+ foreach my $field (@{ $fields || [] }) {
+ next unless defined $data->{$field};
+
+ # Concatenate login names, so that we have a common
+ # way to handle them.
+ my $raw_field;
+ if (ref $data->{$field}) {
+ $raw_field = join(",", @{$data->{$field}});
+ }
+ else {
+ $raw_field = $data->{$field};
+ }
+ $raw_field = clean_text($raw_field || '');
+
+ my @queries = split(/[,;]+/, $raw_field);
+ _create_ldap_user_if_exists($_) foreach @queries;
+ }
+}
+
+sub _bind_ldap_for_search {
+ my $ldap = Bugzilla->ldap;
+ my $bind_result;
+ if (Bugzilla->params->{"LDAPbinddn"}) {
+ my ($LDAPbinddn,$LDAPbindpass) =
+ split(":",Bugzilla->params->{"LDAPbinddn"});
+ $bind_result =
+ $ldap->bind($LDAPbinddn, password => $LDAPbindpass);
+ }
+ else {
+ $bind_result = $ldap->bind();
+ }
+ ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error})
+ if $bind_result->code;
+}
+
+sub _create_ldap_user_if_exists {
+ my ($username) = @_;
+ my $ldap = Bugzilla->ldap;
+
+ _bind_ldap_for_search();
+
+ $username = escape_filter_value($username);
+
+ my $uid_attrib = Bugzilla->params->{LDAPuidattribute};
+ my $mail_attrib = Bugzilla->params->{LDAPmailattribute};
+ my @attrs = ($uid_attrib, $mail_attrib, 'displayName', 'cn');
+
+ my $filter_attrib = '';
+ if ($uid_attrib ne $mail_attrib) {
+ $filter_attrib = $uid_attrib;
+ } else {
+ $filter_attrib = $mail_attrib;
+ }
+
+ my $result = $ldap->search(( base => Bugzilla->params->{LDAPBaseDN},
+ scope => 'sub',
+ filter => "$filter_attrib=$username" ),
+ attrs => \@attrs);
+
+ ThrowCodeError('ldap_search_error',
+ errstr => $result->error, username => $username) if $result->code;
+
+ # We just want the rigth match.
+ return if $result->count != 1;
+
+ my $entry = $result->shift_entry;
+ my $uid = $entry->get_value($uid_attrib);
+ my $email = $entry->get_value($mail_attrib);
+ my $realname ||= $entry->get_value("displayName");
+ $realname ||= $entry->get_value("cn");
+
+ # User already exists in Bugzilla.
+ my $bugz_user = new Bugzilla::User({ extern_id => $uid });
+ return if defined $bugz_user;
+
+ my $password = generate_random_password();
+ trick_taint($password);
+
+ my $ldap_user = Bugzilla::User->create({
+ realname => $realname,
+ login_name => $email,
+ cryptpassword => $password,
+ extern_id => $uid });
+
+ return $ldap_user;
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/LDAPUsers/README.md b/extensions/LDAPUsers/README.md
new file mode 100644
index 0000000..556340c
--- /dev/null
+++ b/extensions/LDAPUsers/README.md
@@ -0,0 +1,4 @@
+LDAPUsers
+=========
+
+Bugzilla extension to enable LDAP user match \ No newline at end of file