# HG changeset patch # User rob@hoelz.ro # Date 1346592950 -7200 # Node ID 1d51c5e38faa8ee3ab233a850c86b16d40878237 # Parent ba2e207e1fb73bea6c1802781cd90ed4870a5fc7 Add LDAP plugin suite diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_auth_ldap2/mod_auth_ldap.lua --- /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); diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_lib_ldap/README.md --- /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! diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_lib_ldap/dev/README.md --- /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 diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_lib_ldap/dev/TODO.md --- /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 diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_lib_ldap/dev/posix-users.ldif --- /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 diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_lib_ldap/dev/prosody-posix-ldap.cfg.lua --- /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', + }, +} diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_lib_ldap/dev/slapd.conf --- /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 diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_lib_ldap/dev/t/00-login.t --- /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); +}; diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_lib_ldap/dev/t/01-rosters.t --- /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'], +}]); diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_lib_ldap/dev/t/02-vcard.t --- /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', +}); diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_lib_ldap/dev/t/TestConnection.pm --- /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; diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_lib_ldap/dev/t/XMPP/TestUtils.pm --- /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; diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_lib_ldap/ldap.lib.lua --- /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; diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_storage_ldap/ldap/vcard.lib.lua --- /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; diff -r ba2e207e1fb7 -r 1d51c5e38faa mod_storage_ldap/mod_storage_ldap.lua --- /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);