--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_auth_ldap2/mod_auth_ldap.lua Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,84 @@
+-- vim:sts=4 sw=4
+
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+-- Copyright (C) 2012 Rob Hoelz
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- http://code.google.com/p/prosody-modules/source/browse/mod_auth_ldap/mod_auth_ldap.lua
+-- adapted to use common LDAP store
+
+local ldap = module:require 'ldap';
+local new_sasl = require 'util.sasl'.new;
+local nodeprep = require 'util.encodings'.stringprep.nodeprep;
+local jsplit = require 'util.jid'.split;
+
+if not ldap then
+ return;
+end
+
+local provider = { name = 'ldap' }
+
+function provider.test_password(username, password)
+ return ldap.bind(username, password);
+end
+
+function provider.user_exists(username)
+ local params = ldap.getparams()
+
+ local filter = ldap.filter.combine_and(params.user.filter, params.user.usernamefield .. '=' .. username);
+
+ return ldap.singlematch {
+ base = params.user.basedn,
+ filter = filter,
+ };
+end
+
+function provider.get_password(username)
+ return nil, "Passwords unavailable for LDAP.";
+end
+
+function provider.set_password(username, password)
+ return nil, "Passwords unavailable for LDAP.";
+end
+
+function provider.create_user(username, password)
+ return nil, "Account creation/modification not available with LDAP.";
+end
+
+function provider.get_sasl_handler()
+ local testpass_authentication_profile = {
+ plain_test = function(sasl, username, password, realm)
+ local prepped_username = nodeprep(username);
+ if not prepped_username then
+ module:log("debug", "NODEprep failed on username: %s", username);
+ return "", nil;
+ end
+ return provider.test_password(prepped_username, password), true;
+ end,
+ mechanisms = { PLAIN = true },
+ };
+ return new_sasl(module.host, testpass_authentication_profile);
+end
+
+function provider.is_admin(jid)
+ local admin_config = ldap.getparams().admin;
+
+ if not admin_config then
+ return;
+ end
+
+ local ld = ldap:getconnection();
+ local username = jsplit(jid);
+ local filter = ldap.filter.combine_and(admin_config.filter, admin_config.namefield .. '=' .. username);
+
+ return ldap.singlematch {
+ base = admin_config.basedn,
+ filter = filter,
+ };
+end
+
+module:add_item("auth-provider", provider);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_lib_ldap/README.md Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,151 @@
+# LDAP plugin suite for Prosody
+
+The LDAP plugin suite includes an authentication plugin (mod\_auth\_ldap2) and storage plugin
+(mod\_storage\_ldap) to query against an LDAP server. It also provides a plugin library (mod\_lib\_ldap)
+for accessing an LDAP server to make writing other LDAP-based plugins easier in the future.
+
+# LDAP Authentication
+
+**NOTE**: LDAP authentication currently only works with plaintext auth! If this isn't ok
+with you, don't use it! (Or better yet, fix it =) )
+
+With that note in mind, you need to set 'allow\_unencrypted\_plain\_auth' to true in your configuration if
+you want to use LDAP authentication.
+
+To enable LDAP authentication, set 'authentication' to 'ldap' in your configuration file.
+See also http://prosody.im/doc/authentication.
+
+# LDAP Storage
+
+LDAP storage is currently read-only, and it only supports rosters and vCards.
+
+To enable LDAP storage, set 'storage' to 'ldap' in your configuration file.
+See also http://prosody.im/doc/storage.
+
+# LDAP Configuration
+
+All of the LDAP-specific configuration for the plugin set goes into an 'ldap' section
+in the configuration. You must set the 'hostname' field in the 'ldap' section to
+your LDAP server's location (a custom port is also accepted, so I guess it's not strictly
+a hostname). The 'bind\_dn' and 'bind\_password' are optional if you want to bind as
+a specific DN. There should be an example configuration included with this README, so
+feel free to consult that.
+
+## The user section
+
+The user section must contain the following keys:
+
+ * basedn - The base DN against which to base your LDAP queries for users.
+ * filter - An LDAP filter expression that matches users.
+ * usernamefield - The name of the attribute in an LDAP entry that contains the username.
+ * namefield - The name of the attribute in an LDAP entry that contains the user's real name.
+
+## The groups section
+
+The LDAP plugin suite has support for grouping (ala mod\_groups), which can be enabled via the groups
+section in the ldap section of the configuration file. Currently, you must have at least one group.
+The groups section must contain the following keys:
+
+ * basedn - The base DN against which to base your LDAP queries for groups.
+ * memberfield - The name of the attribute in an LDAP entry that contains a list of a group's members. The contents of this field
+ must match usernamefield in the user section.
+ * namefield - The name of the attribute in an LDAP entry that contains the group's name.
+
+The groups section must contain at least one entry in its array section. Each entry must be a table, with the following keys:
+
+ * name - The name of the group that will be presented in the roster.
+ * $namefield (whatever namefield is set to is the name) - An attribute pair to match this group against.
+ * admin (optional) - whether or not this group's members are admins.
+
+## The vcard\_format section
+
+The vcard\_format section is used to generate a vCard given an LDAP entry. See http://xmpp.org/extensions/xep-0054.html for
+more information. The JABBERID field is automatically populated.
+
+The key/value pairs in this table fall into three categories:
+
+### Simple pairs
+
+Some values in the vcard\_format table are simple key-value pairs, where the key corresponds to a vCard
+entry, and the value corresponds to the attribute name in the LDAP entry for the user. The fields that
+be configured this way are:
+
+ * displayname - corresponds to FN
+ * nickname - corresponds to NICKNAME
+ * birthday - corresponds to BDAY
+ * mailer - corresponds to MAILER
+ * timezone - corresponds to TZ
+ * title - corresponds to TITLE
+ * role - corresponds to ROLE
+ * note - corresponds to NOTE
+ * rev - corresponds to REV
+ * sortstring - corresponds to SORT-STRING
+ * uid - corresponds to UID
+ * url - corresponds to URL
+ * description - corresponds to DESC
+
+### Single-level fields
+
+These pairs have a table as their values, and the table itself has a series of key value pairs that are translated
+similarly to simple pairs. The fields that are configured this way are:
+
+ * name - corresponds to N
+ * family - corresponds to FAMILY
+ * given - corresponds toGIVEN
+ * middle - corresponds toMIDDLE
+ * prefix - corresponds toPREFIX
+ * suffix - corresponds toSUFFIX
+ * photo - corresponds to PHOTO
+ * type - corresponds to TYPE
+ * binval - corresponds to BINVAL
+ * extval - corresponds to EXTVAL
+ * geo - corresponds to GEO
+ * lat - corresponds to LAT
+ * lon - corresponds to LON
+ * logo - corresponds to LOGO
+ * type - corresponds to TYPE
+ * binval - corresponds to BINVAL
+ * extval - corresponds to EXTVAL
+ * org - corresponds to ORG
+ * orgname - corresponds to ORGNAME
+ * orgunit - corresponds to ORGUNIT
+ * sound - corresponds to SOUND
+ * phonetic - corresponds to PHONETIC
+ * binval - corresponds to BINVAL
+ * extval - corresponds to EXTVAL
+ * key - corresponds to KEY
+ * type - corresponds to TYPE
+ * cred - corresponds to CRED
+
+### Multi-level fields
+
+These pairs have a table as their values, and each table itself has tables as its values. The nested tables have
+the same key-value pairs you're used to, the only difference being that values may have a boolean as their type, which
+converts them into an empty XML tag. I recommend looking at the example configuration for clarification.
+
+ * address - ADR
+ * telephone - TEL
+ * email - EMAIL
+
+### Unsupported vCard fields
+
+ * LABEL
+ * AGENT
+ * CATEGORIES
+ * PRODID
+ * CLASS
+
+### Example Configuration
+
+You can find an example configuration in the dev directory underneath the
+directory that this file is located in.
+
+# Missing Features
+
+This set of plugins is missing a few features, some of which are really just ideas:
+
+ * Implement non-plaintext authentication.
+ * Use proper LDAP binding (LuaLDAP must be patched with http://prosody.im/patches/lualdap.patch, though)
+ * Non-hardcoded LDAP groups (derive groups from LDAP queries)
+ * LDAP-based MUCs (like a private MUC per group, or something)
+ * This suite of plugins was developed with a POSIX-style setup in mind; YMMV. Patches to work with other setups are welcome!
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_lib_ldap/dev/README.md Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,14 @@
+Developer Utilities/Tests
+=========================
+
+This directory exists for reasons of sanity checking. If you wish
+to run the tests, set up Prosody as you normally would, and install the LDAP
+modules as normal as well. Set up OpenLDAP using the configuration directory
+found in this directory (slapd.conf), and run the following command to import
+the test definitions into the LDAP server:
+
+ ldapadd -x -w prosody -D 'cn=Manager,dc=example,dc=com' -f posix-users.ldif
+
+Then just run prove (you will need perl and AnyEvent::XMPP installed):
+
+ prove t
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_lib_ldap/dev/TODO.md Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,2 @@
+ * Make groups optional
+ * Make groups work with both posixGroup and groupOfNames
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_lib_ldap/dev/posix-users.ldif Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,92 @@
+# This is an LDIF file containing simple user definitions for a POSIX-style LDAP
+# setup.
+
+dn: dc=example,dc=com
+objectclass: dcObject
+objectclass: organization
+o: Example
+dc: example
+
+dn: cn=Manager,dc=example,dc=com
+objectclass: organizationalRole
+cn: Manager
+
+dn: ou=Groups,dc=example,dc=com
+ou: Groups
+objectclass: organizationalUnit
+
+dn: ou=Users,dc=example,dc=com
+ou: Users
+objectclass: organizationalUnit
+
+dn: uid=one,ou=Users,dc=example,dc=com
+objectclass: posixAccount
+objectclass: person
+uid: one
+uidNumber: 1000
+gidNumber: 1000
+sn: Testerson
+cn: John Testerson
+userPassword: 12345
+homeDirectory: /home/one
+
+dn: uid=two,ou=Users,dc=example,dc=com
+objectclass: posixAccount
+objectclass: person
+uid: two
+uidNumber: 1001
+gidNumber: 1001
+sn: Testerson
+cn: Jane Testerson
+userPassword: 23451
+homeDirectory: /home/two
+
+dn: uid=three,ou=Users,dc=example,dc=com
+objectclass: posixAccount
+objectclass: person
+uid: three
+uidNumber: 1002
+gidNumber: 1002
+sn: Testerson
+cn: Jerry Testerson
+userPassword: 34512
+homeDirectory: /home/three
+
+dn: uid=four,ou=Users,dc=example,dc=com
+objectclass: posixAccount
+objectclass: person
+uid: four
+uidNumber: 1003
+gidNumber: 1003
+sn: Testerson
+cn: Jack Testerson
+userPassword: 45123
+homeDirectory: /home/four
+
+dn: uid=five,ou=Users,dc=example,dc=com
+objectclass: posixAccount
+objectclass: person
+uid: five
+uidNumber: 1004
+gidNumber: 1004
+sn: Testerson
+cn: Jimmy Testerson
+userPassword: 51234
+homeDirectory: /home/five
+
+dn: cn=Everyone,ou=Groups,dc=example,dc=com
+objectclass: posixGroup
+cn: Everyone
+gidNumber: 2000
+memberUid: one
+memberUid: two
+memberUid: three
+memberUid: four
+memberUid: five
+
+dn: cn=Admin,ou=Groups,dc=example,dc=com
+objectclass: posixGroup
+cn: Admin
+gidNumber: 2001
+memberUid: one
+memberUid: two
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_lib_ldap/dev/prosody-posix-ldap.cfg.lua Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,38 @@
+-- Use Include 'prosody-posix-ldap.cfg.lua' from prosody.cfg.lua to include this file
+authentication = 'ldap' -- Indicate that we want to use LDAP for authentication
+storage = 'ldap' -- Indicate that we want to use LDAP for roster/vcard storage
+
+ldap = {
+ hostname = 'localhost', -- LDAP server location
+ bind_dn = 'cn=Manager,dc=example,dc=com', -- Bind DN for LDAP authentication (optional if anonymous bind is supported)
+ bind_password = 'prosody', -- Bind password (optional if anonymous bind is supported)
+
+ user = {
+ basedn = 'ou=Users,dc=example,dc=com', -- The base DN where user records can be found
+ filter = 'objectClass=posixAccount', -- Filter expression to find user records under basedn
+ usernamefield = 'uid', -- The field that contains the user's ID (this will be the username portion of the JID)
+ namefield = 'cn', -- The field that contains the user's full name (this will be the alias found in the roster)
+ },
+
+ groups = {
+ basedn = 'ou=Groups,dc=example,dc=com', -- The base DN where group records can be found
+ memberfield = 'memberUid', -- The field that contains user ID records for this group (each member must have a corresponding entry under the user basedn with the same value in usernamefield)
+ namefield = 'cn', -- The field that contains the group's name (used for matching groups in LDAP to group definitions below)
+
+ {
+ name = 'everyone', -- The group name that will be seen in users' rosters
+ cn = 'Everyone', -- This field's key *must* match ldap.groups.namefield! It's the name of the LDAP group this definition represents
+ admin = false, -- (Optional) A boolean flag that indicates whether members of this group should be considered administrators.
+ },
+ {
+ name = 'admin',
+ cn = 'Admin',
+ admin = true,
+ },
+ },
+
+ vcard_format = {
+ displayname = 'cn', -- Consult the vCard configuration section in the README
+ nickname = 'uid',
+ },
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_lib_ldap/dev/slapd.conf Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,14 @@
+include /etc/openldap/schema/core.schema
+# I needed the following two schema definitions for posixGroup; if you don't
+# need it, don't include them
+include /etc/openldap/schema/cosine.schema
+include /etc/openldap/schema/nis.schema
+
+pidfile /var/run/openldap/slapd.pid
+argsfile /var/run/openldap/slapd.args
+database bdb
+suffix "dc=example,dc=com"
+rootdn "cn=Manager,dc=example,dc=com"
+rootpw prosody
+directory /var/lib/openldap/openldap-data
+index objectClass eq
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_lib_ldap/dev/t/00-login.t Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,49 @@
+use strict;
+use warnings;
+use lib 't';
+
+use TestConnection;
+use Test::More;
+
+my @users = (
+ 'one',
+ 'two',
+ 'three',
+ 'four',
+ 'five',
+);
+
+plan tests => scalar(@users) + 2;
+
+foreach my $username (@users) {
+ my $conn = TestConnection->new($username);
+
+ $conn->reg_cb(session_ready => sub {
+ $conn->cond->send;
+ });
+
+ my $error = $conn->cond->recv;
+ ok(! $error) or diag($error);
+}
+
+do {
+ my $conn = TestConnection->new('one', password => '23451');
+
+ $conn->reg_cb(session_ready => sub {
+ $conn->cond->send;
+ });
+
+ my $error = $conn->cond->recv;
+ ok($error);
+};
+
+do {
+ my $conn = TestConnection->new('six', password => '12345');
+
+ $conn->reg_cb(session_ready => sub {
+ $conn->cond->send;
+ });
+
+ my $error = $conn->cond->recv;
+ ok($error);
+};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_lib_ldap/dev/t/01-rosters.t Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,136 @@
+use strict;
+use warnings;
+use lib 't';
+
+use AnyEvent::XMPP::Util qw(split_jid);
+use TestConnection;
+use Test::More;
+
+sub test_roster {
+ my ( $username, $expected_contacts ) = @_;
+
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ my @contacts;
+
+ my $conn = TestConnection->new($username);
+
+ $conn->reg_cb(roster_update => sub {
+ my ( undef, $roster ) = @_;
+
+ @contacts = sort { $a->{'username'} cmp $b->{'username'} } map {
+ +{
+ username => (split_jid($_->jid))[0],
+ name => $_->name,
+ groups => [ sort $_->groups ],
+ subscription => $_->subscription,
+ }
+ } $roster->get_contacts;
+ $conn->cond->send;
+ });
+
+ my $error = $conn->cond->recv;
+
+ if($error) {
+ fail($error);
+ return;
+ }
+ @$expected_contacts = sort { $a->{'username'} cmp $b->{'username'} }
+ @$expected_contacts;
+ foreach my $contact (@$expected_contacts) {
+ $contact->{'subscription'} = 'both';
+ @{ $contact->{'groups'} } = sort @{ $contact->{'groups'} };
+ }
+ is_deeply(\@contacts, $expected_contacts);
+}
+
+plan tests => 5;
+
+test_roster(one => [{
+ username => 'two',
+ name => 'Jane Testerson',
+ groups => ['everyone', 'admin'],
+}, {
+ username => 'three',
+ name => 'Jerry Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'four',
+ name => 'Jack Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'five',
+ name => 'Jimmy Testerson',
+ groups => ['everyone'],
+}]);
+
+test_roster(two => [{
+ username => 'one',
+ name => 'John Testerson',
+ groups => ['everyone', 'admin'],
+}, {
+ username => 'three',
+ name => 'Jerry Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'four',
+ name => 'Jack Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'five',
+ name => 'Jimmy Testerson',
+ groups => ['everyone'],
+}]);
+
+test_roster(three => [{
+ username => 'one',
+ name => 'John Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'two',
+ name => 'Jane Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'four',
+ name => 'Jack Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'five',
+ name => 'Jimmy Testerson',
+ groups => ['everyone'],
+}]);
+
+test_roster(four => [{
+ username => 'one',
+ name => 'John Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'two',
+ name => 'Jane Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'three',
+ name => 'Jerry Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'five',
+ name => 'Jimmy Testerson',
+ groups => ['everyone'],
+}]);
+
+test_roster(five => [{
+ username => 'one',
+ name => 'John Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'two',
+ name => 'Jane Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'three',
+ name => 'Jerry Testerson',
+ groups => ['everyone'],
+}, {
+ username => 'four',
+ name => 'Jack Testerson',
+ groups => ['everyone'],
+}]);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_lib_ldap/dev/t/02-vcard.t Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,87 @@
+use strict;
+use warnings;
+use lib 't';
+
+use TestConnection;
+use AnyEvent::XMPP::Ext::VCard;
+use Test::More;
+
+sub test_vcard {
+ my ( $username, $expected_fields ) = @_;
+
+ $expected_fields->{'JABBERID'} = $username . '@' . $TestConnection::HOST;
+ $expected_fields->{'VERSION'} = '2.0';
+
+ my $conn = TestConnection->new($username);
+ my $vcard = AnyEvent::XMPP::Ext::VCard->new;
+
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ $conn->reg_cb(stream_ready => sub {
+ $vcard->hook_on($conn);
+ });
+
+ $conn->reg_cb(session_ready => sub {
+ $vcard->retrieve($conn, undef, sub {
+ my ( $jid, $vcard, $error ) = @_;
+
+ if(eval { $vcard->isa('AnyEvent::XMPP::Error') }) {
+ $error = $vcard;
+ }
+
+ if($error) {
+ $conn->cond->send($error->string);
+ return;
+ }
+
+ foreach my $key (keys %$vcard) {
+ my $value = $vcard->{$key};
+
+ $value = $value->[0];
+
+ if($value eq '') {
+ delete $vcard->{$key};
+ } else {
+ $vcard->{$key} = $value;
+ }
+ }
+
+ is_deeply $expected_fields, $vcard or diag(explain($vcard));
+ $conn->cond->send;
+ });
+ });
+
+ my $error = $conn->cond->recv;
+
+ if($error) {
+ fail($error);
+ return;
+ }
+}
+
+plan tests => 5;
+
+test_vcard(one => {
+ FN => 'John Testerson',
+ NICKNAME => 'one',
+});
+
+test_vcard(two => {
+ FN => 'Jane Testerson',
+ NICKNAME => 'two',
+});
+
+test_vcard(three => {
+ FN => 'Jerry Testerson',
+ NICKNAME => 'three',
+});
+
+test_vcard(four => {
+ FN => 'Jack Testerson',
+ NICKNAME => 'four',
+});
+
+test_vcard(five => {
+ FN => 'Jimmy Testerson',
+ NICKNAME => 'five',
+});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_lib_ldap/dev/t/TestConnection.pm Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,58 @@
+package TestConnection;
+
+use strict;
+use warnings;
+use parent 'AnyEvent::XMPP::IM::Connection';
+
+use 5.010;
+
+our $HOST = 'localhost';
+our $TIMEOUT = 5;
+our %PASSWORD_FOR = (
+ one => '12345',
+ two => '23451',
+ three => '34512',
+ four => '45123',
+ five => '51234',
+);
+
+sub new {
+ my ( $class, $username, %options ) = @_;
+
+ my $cond = AnyEvent->condvar;
+ my $timer = AnyEvent->timer(
+ after => $TIMEOUT,
+ cb => sub {
+ $cond->send('timeout');
+ },
+ );
+
+ my $self = $class->SUPER::new(
+ username => $username,
+ domain => $HOST,
+ password => $options{'password'} // $PASSWORD_FOR{$username},
+ );
+
+ $self->reg_cb(error => sub {
+ my ( undef, $error ) = @_;
+
+ $cond->send($error->string);
+ });
+
+ bless $self, $class;
+
+ $self->{'condvar'} = $cond;
+ $self->{'timeout_timer'} = $timer;
+
+ $self->connect;
+
+ return $self;
+}
+
+sub cond {
+ my ( $self ) = @_;
+
+ return $self->{'condvar'};
+}
+
+1;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_lib_ldap/dev/t/XMPP/TestUtils.pm Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,6 @@
+package XMPP::TestUtils;
+
+use strict;
+use warnings;
+
+1;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_lib_ldap/ldap.lib.lua Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,246 @@
+-- vim:sts=4 sw=4
+
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+-- Copyright (C) 2012 Rob Hoelz
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local ldap;
+local connection;
+local params = module:get_option("ldap");
+local format = string.format;
+local tconcat = table.concat;
+
+local _M = {};
+
+local config_params = {
+ hostname = 'string',
+ user = {
+ basedn = 'string',
+ namefield = 'string',
+ filter = 'string',
+ usernamefield = 'string',
+ },
+ groups = {
+ basedn = 'string',
+ namefield = 'string',
+ memberfield = 'string',
+
+ _member = {
+ name = 'string',
+ admin = 'boolean?',
+ },
+ },
+ admin = {
+ _optional = true,
+ basedn = 'string',
+ namefield = 'string',
+ filter = 'string',
+ }
+}
+
+local function run_validation(params, config, prefix)
+ prefix = prefix or '';
+
+ -- verify that every required member of config is present in params
+ for k, v in pairs(config) do
+ if type(k) == 'string' and k:sub(1, 1) ~= '_' then
+ local is_optional;
+ if type(v) == 'table' then
+ is_optional = v._optional;
+ else
+ is_optional = v:sub(-1) == '?';
+ end
+
+ if not is_optional and params[k] == nil then
+ return nil, prefix .. k .. ' is required';
+ end
+ end
+ end
+
+ for k, v in pairs(params) do
+ local expected_type = config[k];
+
+ local ok, err = true;
+
+ if type(k) == 'string' then
+ -- verify that this key is present in config
+ if k:sub(1, 1) == '_' or expected_type == nil then
+ return nil, 'invalid parameter ' .. prefix .. k;
+ end
+
+ -- type validation
+ if type(expected_type) == 'string' then
+ if expected_type:sub(-1) == '?' then
+ expected_type = expected_type:sub(1, -2);
+ end
+
+ if type(v) ~= expected_type then
+ return nil, 'invalid type for parameter ' .. prefix .. k;
+ end
+ else -- it's a table (or had better be)
+ if type(v) ~= 'table' then
+ return nil, 'invalid type for parameter ' .. prefix .. k;
+ end
+
+ -- recurse into child
+ ok, err = run_validation(v, expected_type, prefix .. k .. '.');
+ end
+ else -- it's an integer (or had better be)
+ if not config._member then
+ return nil, 'invalid parameter ' .. prefix .. tostring(k);
+ end
+ ok, err = run_validation(v, config._member, prefix .. tostring(k) .. '.');
+ end
+
+ if not ok then
+ return ok, err;
+ end
+ end
+
+ return true;
+end
+
+local function validate_config()
+ if true then
+ return true; -- XXX for now
+ end
+
+ -- this is almost too clever (I mean that in a bad
+ -- maintainability sort of way)
+ --
+ -- basically this allows a free pass for a key in group members
+ -- equal to params.groups.namefield
+ setmetatable(config_params.groups._member, {
+ __index = function(_, k)
+ if k == params.groups.namefield then
+ return 'string';
+ end
+ end
+ });
+
+ local ok, err = run_validation(params, config_params);
+
+ setmetatable(config_params.groups._member, nil);
+
+ if ok then
+ -- a little extra validation that doesn't fit into
+ -- my recursive checker
+ local group_namefield = params.groups.namefield;
+ for i, group in ipairs(params.groups) do
+ if not group[group_namefield] then
+ return nil, format('groups.%d.%s is required', i, group_namefield);
+ end
+ end
+
+ -- fill in params.admin if you can
+ if not params.admin and params.groups then
+ local admingroup;
+
+ for _, groupconfig in ipairs(params.groups) do
+ if groupconfig.admin then
+ admingroup = groupconfig;
+ break;
+ end
+ end
+
+ if admingroup then
+ params.admin = {
+ basedn = params.groups.basedn,
+ namefield = params.groups.memberfield,
+ filter = group_namefield .. '=' .. admingroup[group_namefield],
+ };
+ end
+ end
+ end
+
+ return ok, err;
+end
+
+-- what to do if connection isn't available?
+local function connect()
+ return ldap.open_simple(params.hostname, params.bind_dn, params.bind_password, params.use_tls);
+end
+
+-- this is abstracted so we can maintain persistent connections at a later time
+function _M.getconnection()
+ return connect();
+end
+
+function _M.getparams()
+ return params;
+end
+
+-- XXX consider renaming this...it doesn't bind the current connection
+function _M.bind(username, password)
+ local who = format('%s=%s,%s', params.user.usernamefield, username, params.user.basedn);
+ local conn, err = ldap.open_simple(params.hostname, who, password, params.use_tls);
+
+ if conn then
+ conn:close();
+ return true;
+ end
+
+ return conn, err;
+end
+
+function _M.singlematch(query)
+ local ld = _M.getconnection();
+
+ query.sizelimit = 1;
+ query.scope = 'onelevel';
+
+ for dn, attribs in ld:search(query) do
+ return attribs;
+ end
+end
+
+_M.filter = {};
+
+function _M.filter.combine_and(...)
+ local parts = { '(&' };
+
+ local arg = { ... };
+
+ for _, filter in ipairs(arg) do
+ if filter:sub(1, 1) ~= '(' and filter:sub(-1) ~= ')' then
+ filter = '(' .. filter .. ')'
+ end
+ parts[#parts + 1] = filter;
+ end
+
+ parts[#parts + 1] = ')';
+
+ return tconcat(parts, '');
+end
+
+do
+ local ok, err;
+
+ prosody.unlock_globals();
+ ok, ldap = pcall(require, 'lualdap');
+ prosody.lock_globals();
+ if not ok then
+ module:log("error", "Failed to load the LuaLDAP library for accessing LDAP: %s", ldap);
+ module:log("error", "More information on install LuaLDAP can be found at http://www.keplerproject.org/lualdap");
+ return;
+ end
+
+ if not params then
+ module:log("error", "LDAP configuration required to use the LDAP storage module");
+ return;
+ end
+
+ ok, err = validate_config();
+
+ if not ok then
+ module:log("error", "LDAP configuration is invalid: %s", tostring(err));
+ return;
+ end
+end
+
+return _M;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_storage_ldap/ldap/vcard.lib.lua Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,131 @@
+-- vim:sts=4 sw=4
+
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+-- Copyright (C) 2012 Rob Hoelz
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require 'util.stanza';
+
+local VCARD_NS = 'vcard-temp';
+
+local builder_methods = {};
+
+function builder_methods:addvalue(key, value)
+ self.vcard:tag(key):text(value):up();
+end
+
+function builder_methods:addregularfield(tagname, format_section)
+ local record = self.record;
+ local format = self.format;
+ local vcard = self.vcard;
+
+ if not format[format_section] then
+ return;
+ end
+
+ local tag = vcard:tag(tagname);
+
+ for k, v in pairs(format[format_section]) do
+ tag:tag(string.upper(k)):text(record[v]):up();
+ end
+
+ vcard:up();
+end
+
+function builder_methods:addmultisectionedfield(tagname, format_section)
+ local record = self.record;
+ local format = self.format;
+ local vcard = self.vcard;
+
+ if not format[format_section] then
+ return;
+ end
+
+ for k, v in pairs(format[format_section]) do
+ local tag = vcard:tag(tagname);
+
+ if type(k) == 'string' then
+ tag:tag(string.upper(k)):up();
+ end
+
+ for k2, v2 in pairs(v) do
+ if type(v2) == 'boolean' then
+ tag:tag(string.upper(k2)):up();
+ else
+ tag:tag(string.upper(k2)):text(record[v2]):up();
+ end
+ end
+
+ vcard:up();
+ end
+end
+
+function builder_methods:build()
+ local record = self.record;
+ local format = self.format;
+
+ self:addvalue( 'VERSION', '2.0');
+ self:addvalue( 'FN', record[format.displayname]);
+ self:addregularfield( 'N', 'name');
+ self:addvalue( 'NICKNAME', record[format.nickname]);
+ self:addregularfield( 'PHOTO', 'photo');
+ self:addvalue( 'BDAY', record[format.birthday]);
+ self:addmultisectionedfield('ADR', 'address');
+ self:addvalue( 'LABEL', nil); -- we don't support LABEL...yet.
+ self:addmultisectionedfield('TEL', 'telephone');
+ self:addmultisectionedfield('EMAIL', 'email');
+ self:addvalue( 'JABBERID', record.jid);
+ self:addvalue( 'MAILER', record[format.mailer]);
+ self:addvalue( 'TZ', record[format.timezone]);
+ self:addregularfield( 'GEO', 'geo');
+ self:addvalue( 'TITLE', record[format.title]);
+ self:addvalue( 'ROLE', record[format.role]);
+ self:addregularfield( 'LOGO', 'logo');
+ self:addvalue( 'AGENT', nil); -- we don't support AGENT...yet.
+ self:addregularfield( 'ORG', 'org');
+ self:addvalue( 'CATEGORIES', nil); -- we don't support CATEGORIES...yet.
+ self:addvalue( 'NOTE', record[format.note]);
+ self:addvalue( 'PRODID', nil); -- we don't support PRODID...yet.
+ self:addvalue( 'REV', record[format.rev]);
+ self:addvalue( 'SORT-STRING', record[format.sortstring]);
+ self:addregularfield( 'SOUND', 'sound');
+ self:addvalue( 'UID', record[format.uid]);
+ self:addvalue( 'URL', record[format.url]);
+ self:addvalue( 'CLASS', nil); -- we don't support CLASS...yet.
+ self:addregularfield( 'KEY', 'key');
+ self:addvalue( 'DESC', record[format.description]);
+
+ return self.vcard;
+end
+
+local function new_builder(params)
+ local vcard_tag = st.stanza('vCard', { xmlns = VCARD_NS });
+
+ local object = {
+ vcard = vcard_tag,
+ __index = builder_methods,
+ };
+
+ for k, v in pairs(params) do
+ object[k] = v;
+ end
+
+ setmetatable(object, object);
+
+ return object;
+end
+
+local _M = {};
+
+function _M.create(params)
+ local builder = new_builder(params);
+
+ return builder:build();
+end
+
+return _M;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_storage_ldap/mod_storage_ldap.lua Sun Sep 02 15:35:50 2012 +0200
@@ -0,0 +1,180 @@
+-- vim:sts=4 sw=4
+
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+-- Copyright (C) 2012 Rob Hoelz
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+----------------------------------------
+-- Constants and such --
+----------------------------------------
+
+local setmetatable = setmetatable;
+local ldap = module:require 'ldap';
+local vcardlib = module:require 'ldap/vcard';
+local st = require 'util.stanza';
+local gettime = require 'socket'.gettime;
+
+if not ldap then
+ return;
+end
+
+local CACHE_EXPIRY = 300;
+local params = module:get_option('ldap');
+
+----------------------------------------
+-- Utility Functions --
+----------------------------------------
+
+local function ldap_record_to_vcard(record)
+ return vcardlib.create {
+ record = record,
+ format = params.vcard_format,
+ }
+end
+
+local get_alias_for_user;
+
+do
+ local user_cache;
+ local last_fetch_time;
+
+ local function populate_user_cache()
+ local ld = ldap.getconnection();
+
+ local usernamefield = params.user.usernamefield;
+ local namefield = params.user.namefield;
+
+ user_cache = {};
+
+ for _, attrs in ld:search { base = params.user.basedn, scope = 'onelevel', filter = params.user.filter } do
+ user_cache[attrs[usernamefield]] = attrs[namefield];
+ end
+ last_fetch_time = gettime();
+ end
+
+ function get_alias_for_user(user)
+ if last_fetch_time and last_fetch_time + CACHE_EXPIRY < gettime() then
+ user_cache = nil;
+ end
+ if not user_cache then
+ populate_user_cache();
+ end
+ return user_cache[user];
+ end
+end
+
+----------------------------------------
+-- General Setup --
+----------------------------------------
+
+local ldap_store = {};
+ldap_store.__index = ldap_store;
+
+local adapters = {
+ roster = {},
+ vcard = {},
+}
+
+for k, v in pairs(adapters) do
+ setmetatable(v, ldap_store);
+ v.__index = v;
+ v.name = k;
+end
+
+function ldap_store:get(username)
+ return nil, "get method unimplemented on store '" .. tostring(self.name) .. "'"
+end
+
+function ldap_store:set(username, data)
+ return nil, "LDAP storage is currently read-only";
+end
+
+----------------------------------------
+-- Roster Storage Implementation --
+----------------------------------------
+
+function adapters.roster:get(username)
+ local ld = ldap.getconnection();
+ local contacts = {};
+
+ local memberfield = params.groups.memberfield;
+ local namefield = params.groups.namefield;
+ local filter = memberfield .. '=' .. tostring(username);
+
+ local groups = {};
+ for _, config in ipairs(params.groups) do
+ groups[ config[namefield] ] = config.name;
+ end
+
+ -- XXX this kind of relies on the way we do groups at INOC
+ for _, attrs in ld:search { base = params.groups.basedn, scope = 'onelevel', filter = filter } do
+ if groups[ attrs[namefield] ] then
+ local members = attrs[memberfield];
+
+ for _, user in ipairs(members) do
+ if user ~= username then
+ local jid = user .. '@' .. module.host;
+ local record = contacts[jid];
+
+ if not record then
+ record = {
+ subscription = 'both',
+ groups = {},
+ name = get_alias_for_user(user),
+ };
+ contacts[jid] = record;
+ end
+
+ record.groups[ groups[ attrs[namefield] ] ] = true;
+ end
+ end
+ end
+ end
+
+ return contacts;
+end
+
+----------------------------------------
+-- vCard Storage Implementation --
+----------------------------------------
+
+function adapters.vcard:get(username)
+ if not params.vcard_format then
+ return nil, '';
+ end
+
+ local ld = ldap.getconnection();
+ local filter = params.user.usernamefield .. '=' .. tostring(username);
+
+ local match = ldap.singlematch {
+ base = params.user.basedn,
+ filter = filter,
+ };
+ if match then
+ match.jid = username .. '@' .. module.host
+ return st.preserialize(ldap_record_to_vcard(match));
+ else
+ return nil, 'not found';
+ end
+end
+
+----------------------------------------
+-- Driver Definition --
+----------------------------------------
+
+local driver = { name = "ldap" };
+
+function driver:open(store, typ)
+ local adapter = adapters[store];
+
+ if adapter and not typ then
+ return adapter;
+ end
+ return nil, "unsupported-store";
+end
+module:add_item("data-driver", driver);