Diff

mod_lib_ldap/ldap.lib.lua @ 809:1d51c5e38faa

Add LDAP plugin suite
author rob@hoelz.ro
date Sun, 02 Sep 2012 15:35:50 +0200
child 864:16b007c7706c
line wrap: on
line diff
--- /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;