# HG changeset patch # User bsw # Date 1405712579 -7200 # Node ID 58f48a8a202a1d4d8457baf7ba1ee48ea1222dad # Parent effce9b361b2959515f79ebec02f3fe5ed1f8e38 Imported and merged LDAP patch diff -r effce9b361b2 -r 58f48a8a202a app/main/_filter/21_auth.lua --- a/app/main/_filter/21_auth.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/app/main/_filter/21_auth.lua Fri Jul 18 21:42:59 2014 +0200 @@ -9,6 +9,7 @@ or action == "login" or view == "register" or action == "register" + or action == "cancel_register" or view == "about" or view == "reset_password" or action == "reset_password" diff -r effce9b361b2 -r 58f48a8a202a app/main/_filter_view/30_navigation.lua --- a/app/main/_filter_view/30_navigation.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/app/main/_filter_view/30_navigation.lua Fri Jul 18 21:42:59 2014 +0200 @@ -46,6 +46,9 @@ } slot.put ( " " ) + end + + if app.session.member == nil and not config.registration_disabled then ui.link { text = _"Registration", diff -r effce9b361b2 -r 58f48a8a202a app/main/index/_action/cancel_register.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/main/index/_action/cancel_register.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,5 @@ +app.session.authority = nil +app.session.authority_data = nil +app.session:save() + +return true \ No newline at end of file diff -r effce9b361b2 -r 58f48a8a202a app/main/index/_action/login.lua --- a/app/main/index/_action/login.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/app/main/index/_action/login.lua Fri Jul 18 21:42:59 2014 +0200 @@ -1,7 +1,22 @@ local login = param.get("login") local password = param.get("password") -local member = Member:by_login_and_password(login, password) +local member, err, uid = Member:by_login_and_password(login, password) + +if err == "ldap_credentials_valid_but_no_member" then + app.session.authority = "ldap" + app.session.authority_data = encode.pg_hstore{ + login = login, + uid = uid + } + app.session:save() + request.redirect{ + module = "index", view = "register", params = { + ldap_login = login + } + } + return +end function do_etherpad_auth(member) local result = net.curl( diff -r effce9b361b2 -r 58f48a8a202a app/main/index/_action/register.lua --- a/app/main/index/_action/register.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/app/main/index/_action/register.lua Fri Jul 18 21:42:59 2014 +0200 @@ -1,12 +1,26 @@ local code = util.trim(param.get("code")) -local member = Member:new_selector() - :add_where{ "invite_code = ?", code } - :add_where{ "activated ISNULL" } - :add_where{ "NOT locked" } - :optional_object_mode() - :for_update() - :exec() +local member + +if app.session.authority == "ldap" then + if not config.ldap.member or not config.ldap.member.registration == "manual" then + error("access denied") + end + member = ldap.create_member(app.session.authority_data_uid, true) + +else + if config.registration_disabled then + error("registration disabled") + end + member = Member:new_selector() + :add_where{ "invite_code = ?", code } + :add_where{ "activated ISNULL" } + :add_where{ "NOT locked" } + :optional_object_mode() + :for_update() + :exec() +end + if not member then slot.put_into("error", _"The code you've entered is invalid") @@ -20,7 +34,7 @@ local notify_email = param.get("notify_email") -if not config.locked_profile_fields.notify_email and notify_email then +if not util.is_profile_field_locked(member, "notify_email") and notify_email then if #notify_email < 5 then slot.put_into("error", _"Email address too short!") request.redirect{ @@ -33,7 +47,7 @@ end end -if member and not notify_email then +if member and not util.is_profile_field_locked(member, "notify_email") and not notify_email then request.redirect{ mode = "redirect", module = "index", @@ -46,7 +60,7 @@ local name = util.trim(param.get("name")) -if not config.locked_profile_fields.name and name then +if not util.is_profile_field_locked(member, "name") and name then if #name < 3 then slot.put_into("error", _"This screen name is too short!") @@ -83,7 +97,7 @@ end -if notify_email and not member.name then +if notify_email and not util.is_profile_field_locked(member, "name") and not member.name then request.redirect{ mode = "redirect", module = "index", @@ -97,10 +111,9 @@ return false end - local login = util.trim(param.get("login")) -if not config.locked_profile_fields.login and login then +if not util.is_profile_field_locked(member, "login") and login then if #login < 3 then slot.put_into("error", _"This login is too short!") request.redirect{ @@ -136,7 +149,7 @@ member.login = login end -if member.name and not member.login then +if member.name and not util.is_profile_field_locked(member, "login") and not member.login then request.redirect{ mode = "redirect", module = "index", @@ -163,40 +176,43 @@ end end - local password1 = param.get("password1") - local password2 = param.get("password2") + if not member.authority == "ldap" then + + local password1 = param.get("password1") + local password2 = param.get("password2") - if login and not password1 then - request.redirect{ - mode = "redirect", - module = "index", - view = "register", - params = { - code = member.invite_code, - notify_email = notify_email, - name = member.name, - login = member.login + if login and not password1 then + request.redirect{ + mode = "redirect", + module = "index", + view = "register", + params = { + code = member.invite_code, + notify_email = notify_email, + name = member.name, + login = member.login + } } - } - --]] - return false + --]] + return false + end + + if password1 ~= password2 then + slot.put_into("error", _"Passwords don't match!") + return false + end + + if #password1 < 8 then + slot.put_into("error", _"Passwords must consist of at least 8 characters!") + return false + end end - if password1 ~= password2 then - slot.put_into("error", _"Passwords don't match!") - return false - end - - if #password1 < 8 then - slot.put_into("error", _"Passwords must consist of at least 8 characters!") - return false - end - - if not config.locked_profile_fields.login then + if not util.is_profile_field_locked(member, "login") then member.login = login end - if not config.locked_profile_fields.name then + if not util.is_profile_field_locked(member, "name") then member.name = name end @@ -208,7 +224,9 @@ end end - member:set_password(password1) + if not member.authority == "ldap" then + member:set_password(password1) + end local now = db:query("SELECT now() AS now", "object").now @@ -221,7 +239,7 @@ member.active = true member.last_activity = 'now' member:save() - + slot.put_into("notice", _"You've successfully registered and you can login now with your login and password!") request.redirect{ diff -r effce9b361b2 -r 58f48a8a202a app/main/index/register.lua --- a/app/main/index/register.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/app/main/index/register.lua Fri Jul 18 21:42:59 2014 +0200 @@ -1,3 +1,13 @@ +local ldap_uid + +if config.ldap.member and app.session.authority == "ldap" then + ldap_uid = app.session.authority_data_uid +end + +if config.registration_disabled and not ldap_uid then + error("registration disabled") +end + execute.view{ module = "index", view = "_lang_chooser" } local step = param.get("step", atom.integer) @@ -6,6 +16,7 @@ local name = param.get("name") local login = param.get("login") + ui.form{ attr = { class = "section vertical" }, module = 'index', @@ -18,7 +29,7 @@ }, content = function() - if not code then + if not code and not ldap_uid then ui.field.hidden{ name = "step", value = 1 } ui.title(_"Registration (step 1 of 3: Invite code)") ui.sectionHead( function() @@ -37,17 +48,32 @@ ui.link{ content = _"cancel registration", module = "index", - view = "index" + action = "cancel_register", + routing = { default = { + mode = "redirect", module = "index", view = "index" + } } } end ) else - local member = Member:new_selector() - :add_where{ "invite_code = ?", code } - :add_where{ "activated ISNULL" } - :optional_object_mode() - :exec() + local member + + if ldap_uid then + member, err = ldap.create_member(ldap_uid, true) + if err then + error(err) + end + else + member = Member:new_selector() + :add_where{ "invite_code = ?", code } + :add_where{ "activated ISNULL" } + :optional_object_mode() + :exec() + end - if not member.notify_email and not notify_email or not member.name and not name or not member.login and not login or step == 1 then + if (not member.notify_email and not notify_email) + or (not member.name and not name) + or (not member.login and not login and not member.authority) + or step == 1 then ui.title(_"Registration (step 2 of 3: Personal information)") ui.field.hidden{ name = "step", value = 2 } @@ -62,7 +88,7 @@ execute.view{ module = "member", view = "_profile", params = { member = member, for_registration = true } } - if not config.locked_profile_fields.notify_email then + if not util.is_profile_field_locked(member, "notify_email") then ui.tag{ tag = "p", content = _"Please enter your email address. This address will be used for automatic notifications (if you request them) and in case you've lost your password. This address will not be published. After registration you will receive an email with a confirmation link." @@ -73,7 +99,7 @@ value = param.get("notify_email") or member.notify_email } end - if not config.locked_profile_fields.name then + if not util.is_profile_field_locked(member, "name") then ui.tag{ tag = "p", content = _"Please choose a name, i.e. your real name or your nick name. This name will be shown to others to identify you." @@ -84,7 +110,7 @@ value = param.get("name") or member.name } end - if not config.locked_profile_fields.login then + if not util.is_profile_field_locked(member, "login") then ui.tag{ tag = "p", content = _"Please choose a login name. This name will not be shown to others and is used only by you to login into the system. The login name is case sensitive." @@ -111,7 +137,10 @@ ui.link{ content = _"cancel registration", module = "index", - view = "index" + action = "cancel_register", + routing = { default = { + mode = "redirect", module = "index", view = "index" + } } } end ) else @@ -165,24 +194,27 @@ slot.put("
") - ui.tag{ - tag = "p", - content = _"Please choose a password and enter it twice. The password is case sensitive." - } - ui.field.text{ - readonly = true, - label = _'Login name', - name = 'login', - value = member.login - } - ui.field.password{ - label = _'Password', - name = 'password1', - } - ui.field.password{ - label = _'Password (repeat)', - name = 'password2', - } + if not member.authority == "ldap" then + ui.tag{ + tag = "p", + content = _"Please choose a password and enter it twice. The password is case sensitive." + } + ui.field.text{ + readonly = true, + label = _'Login name', + name = 'login', + value = member.login + } + ui.field.password{ + label = _'Password', + name = 'password1', + } + ui.field.password{ + label = _'Password (repeat)', + name = 'password2', + } + end + ui.submit{ text = _'activate account' } @@ -203,7 +235,10 @@ ui.link{ content = _"cancel registration", module = "index", - view = "index" + action = "cancel_register", + routing = { default = { + mode = "redirect", module = "index", view = "index" + } } } end ) end diff -r effce9b361b2 -r 58f48a8a202a app/main/member/_action/update.lua --- a/app/main/member/_action/update.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/app/main/member/_action/update.lua Fri Jul 18 21:42:59 2014 +0200 @@ -17,12 +17,12 @@ local update_args = { app.session.member } for i, field in ipairs(fields) do - if not config.locked_profile_fields[field] then + if not util.is_profile_field_locked(app.session.member, field) then param.update(app.session.member, field) end end -if not config.locked_profile_fields.statement then +if not util.is_profile_field_locked(app.session.member, "statement") then local formatting_engine = param.get("formatting_engine") or config.enforce_formatting_engine local formatting_engine_valid = false @@ -47,7 +47,7 @@ end -if not config.locked_profile_fields.birthday then +if not util.is_profile_field_locked(app.session.member, "birthday") then if tostring(app.session.member.birthday) == "invalid_date" then app.session.member.birthday = nil slot.put_into("error", _"Date format is not valid. Please use following format: YYYY-MM-DD") diff -r effce9b361b2 -r 58f48a8a202a app/main/member/_action/update_email.lua --- a/app/main/member/_action/update_email.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/app/main/member/_action/update_email.lua Fri Jul 18 21:42:59 2014 +0200 @@ -1,6 +1,6 @@ local resend = param.get("resend", atom.boolean) -if not resend and config.locked_profile_fields.notify_email then +if not resend and util.is_profile_field_locked(app.session.member, "notify_email") then error("access denied") end diff -r effce9b361b2 -r 58f48a8a202a app/main/member/_action/update_login.lua --- a/app/main/member/_action/update_login.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/app/main/member/_action/update_login.lua Fri Jul 18 21:42:59 2014 +0200 @@ -1,4 +1,4 @@ -if config.locked_profile_fields.login then +if util.is_profile_field_locked(app.session.member, "login") then error("access denied") end diff -r effce9b361b2 -r 58f48a8a202a app/main/member/_action/update_name.lua --- a/app/main/member/_action/update_name.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/app/main/member/_action/update_name.lua Fri Jul 18 21:42:59 2014 +0200 @@ -1,4 +1,4 @@ -if config.locked_profile_fields.name then +if util.is_profile_field_locked(app.session.member, "name") then error("access denied") end diff -r effce9b361b2 -r 58f48a8a202a app/main/member/_profile.lua --- a/app/main/member/_profile.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/app/main/member/_profile.lua Fri Jul 18 21:42:59 2014 +0200 @@ -104,7 +104,7 @@ if member.last_activity then ui.field.text{ label = _"Last activity (updated daily)", value = format.date(member.last_activity) or _"not yet" } end - if member.statement and #member.statement > 0 then + if member.id and member.statement and #member.statement > 0 then slot.put("
") slot.put("
") ui.container{ diff -r effce9b361b2 -r 58f48a8a202a app/main/member/_sidebar_whatcanido.lua --- a/app/main/member/_sidebar_whatcanido.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/app/main/member/_sidebar_whatcanido.lua Fri Jul 18 21:42:59 2014 +0200 @@ -69,16 +69,18 @@ local pages = {} pages[#pages+1] = { view = "settings_notification", text = _"notification settings" } - if not config.locked_profile_fields.notify_email then + if not util.is_profile_field_locked(app.session.member, "notify_email") then pages[#pages+1] = { view = "settings_email", text = _"change your notification email address" } end - if not config.locked_profile_fields.name then + if not util.is_profile_field_locked(app.session.member, "name") then pages[#pages+1] = { view = "settings_name", text = _"change your screen name" } end - if not config.locked_profile_fields.login then + if not util.is_profile_field_locked(app.session.member, "login") then pages[#pages+1] = { view = "settings_login", text = _"change your login" } end - pages[#pages+1] = { view = "settings_password", text = _"change your password" } + if not util.is_profile_field_locked(app.session.member, "password") then + pages[#pages+1] = { view = "settings_password", text = _"change your password" } + end pages[#pages+1] = { view = "developer_settings", text = _"developer settings" } if config.download_dir then diff -r effce9b361b2 -r 58f48a8a202a config/example.lua --- a/config/example.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/config/example.lua Fri Jul 18 21:42:59 2014 +0200 @@ -96,6 +96,14 @@ -- Remove leading -- to use a option -- ======================================================================== +-- Disable registration +-- ------------------------------------------------------------------------ +-- Available options: +-- false: registration is enabled (default) +-- true: registration is disabled +-- ------------------------------------------------------------------------ +-- config.disable_registration = true + -- List of enabled languages, defaults to available languages -- ------------------------------------------------------------------------ -- config.enabled_languages = { 'en', 'de', 'eo', 'el', 'hu', 'it', 'nl', 'zh-Hans', 'zh-TW' } diff -r effce9b361b2 -r 58f48a8a202a config/init.lua --- a/config/init.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/config/init.lua Fri Jul 18 21:42:59 2014 +0200 @@ -3,7 +3,7 @@ -- (except when you really know what you are doing!) -- ======================================================================== -config.app_version = "3.0.2" +config.app_version = "3.0.2+ldap" if not config.password_hash_algorithm then config.password_hash_algorithm = "crypt_sha512" @@ -48,6 +48,10 @@ config.check_delegations_default = "confirm" end +if config.ldap == nil then + config.ldap = {} +end + if not config.database then config.database = { engine='postgresql', dbname='liquid_feedback' } end diff -r effce9b361b2 -r 58f48a8a202a env/encode/pg_hstore.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/encode/pg_hstore.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,16 @@ +-- Encodes a Lua table as PostgreSQL hstore text input +-- TODO This should be implemented in the SQL abstraction layer + +function encode.pg_hstore(hstore_values) + + local entries = {} + + for key, val in pairs(hstore_values) do + local escaped_key = encode.pg_hstore_value(key) + local escaped_val = encode.pg_hstore_value(val) + entries[#entries+1] = escaped_key .. "=>" .. escaped_val + end + + return table.concat(entries, ", ") + +end diff -r effce9b361b2 -r 58f48a8a202a env/encode/pg_hstore_value.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/encode/pg_hstore_value.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,6 @@ +-- Formats a value (or a key) for usage in the text representation of +-- hstore fields + +function encode.pg_hstore_value(value) + return '"' .. string.gsub(value, '([\\"])', "\\%1") .. '"' +end \ No newline at end of file diff -r effce9b361b2 -r 58f48a8a202a env/ldap/__init.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/ldap/__init.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,2 @@ +-- Lua library path for C modules for mldap +package.cpath = request.get_app_basepath() .. "/lib/mldap/?.so" .. ";" .. package.cpath \ No newline at end of file diff -r effce9b361b2 -r 58f48a8a202a env/ldap/bind.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/ldap/bind.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,55 @@ +-- binds to configured LDAP server +-- -------------------------------------------------------------------------- +-- omit arguments for anonymous bind +-- +-- arguments: +-- dn: the distinguished name to be used fo binding (string) +-- password: password credentials (string) +-- +-- returns: +-- ldap: in case of success, an LDAP connection handle +-- err: in case of an error, an error code (string) +-- err2: error dependent extra error information + +function ldap.bind(dn, password) + + local libldap = require("mldap") + + local hostlist = ldap.get_hosts() + + -- try binding to LDAP server until success of no host entry left + local ldap + while not ldap do + + if #hostlist < 1 then + break + end + + local host = table.remove(hostlist, 1) + + local err + ldap, err, errno = libldap.bind{ + uri = host.uri, + timeout = host.timeout, + who = dn, + password = password + } + + if not err and ldap then + return ldap, nil + end + + local errno_string + + if errno then + errno_string = libldap.errorcodes[errno] + end + + if errno == libldap.errorcodes.invalid_credentials then + return nil, "invalid_credentials", errno_string + end + end + + return nil, "cant_contact_ldap_server" + +end diff -r effce9b361b2 -r 58f48a8a202a env/ldap/bind_as_app.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/ldap/bind_as_app.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,21 @@ +-- binds to configured LDAP server with application's credentials +-- -------------------------------------------------------------------------- +-- +-- returns +-- ldap_conn: in case of success, an LDAP connection handle +-- err: in case of an error, an error code (string) + +function ldap.bind_as_app() + + local dn, password + + if config.ldap.bind_as then + dn = config.ldap.bind_as.dn + password = config.ldap.bind_as.password + end + + local ldap_conn, err = ldap.bind(dn, password) + + return ldap_conn, err + +end diff -r effce9b361b2 -r 58f48a8a202a env/ldap/check_credentials.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/ldap/check_credentials.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,47 @@ +-- check if credentials (given by a user) are valid to bind to LDAP +-- -------------------------------------------------------------------------- +-- +-- arguments: +-- dn: The distinguished name to be used fo binding (string, required) +-- password: Password credentials (string, required) +-- +-- returns +-- success: true in cases of valid credentials +-- false in cases of invalid credentials +-- nil in undetermined cases, i.e. unavailable LDAP server +-- err: error code in case of errors, otherwise nil (string) +-- err2: error dependent extra error information + +function ldap.check_credentials(login, password) + + local filter = config.ldap.member.login_filter_map(login) + local ldap_entry, err, err2 = ldap.get_member_entry(filter) + + if err == "too_many_entries_found" then + return false, "invalid_credentials" + end + + if err then + return nil, err + end + if not ldap_entry then + return false, "invalid_credentials" + end + + local dn = ldap_entry.dn + + local ldap, err, err2 = ldap.bind(dn, password) + + if err == "invalid_credentials" then + return false, "invalid_credentials" + end + + if err then + return nil, err, err2 + end + + ldap:unbind() + + return ldap_entry + +end diff -r effce9b361b2 -r 58f48a8a202a env/ldap/create_member.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/ldap/create_member.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,39 @@ +-- Create a new member object from LDAP for an uid +-- -------------------------------------------------------------------------- +-- +-- arguments: +-- uid: uid of the new member object (required) +-- +-- returns: +-- member: a LiquidFeedback Member object (in case of success) +-- err: error code in case of an error (string) +-- err2: error dependent extra error information + +function ldap.create_member(uid) + + local member = Member:new() + + member.authority = "ldap" + + member.authority_data = encode.pg_hstore{ + uid = uid + } + + local ldap_conn, ldap_entry, err, err2 = ldap.update_member_attr(member, nil, uid) + + if ldap_conn then + ldap_conn:unbind() + end + + member.authority_data = encode.pg_hstore{ + uid = uid, + login = config.ldap.member.login_map(ldap_entry) + } + + if not err then + return member + end + + return nil, err, err2 + +end diff -r effce9b361b2 -r 58f48a8a202a env/ldap/escape_filter.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/ldap/escape_filter.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,20 @@ +-- Escape a string to be used as safe LDAP filter +-- -------------------------------------------------------------------------- +-- +-- arguments: +-- filter: the string to be escaped (required) +-- +-- returns: +-- escaped_filter: the escaped result + +function ldap.escape_filter(filter) + + local null_pattern = (_VERSION == "Lua 5.1") and "%z" or "\000" + + return string.gsub(filter, "[\\%*%(%)\128-\255" .. null_pattern .. "]", function (char) + + return string.format("%02x", string.byte(char)) + + end) + +end \ No newline at end of file diff -r effce9b361b2 -r 58f48a8a202a env/ldap/get_hosts.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/ldap/get_hosts.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,46 @@ +-- generate a ready-to-use list of the configures LDAP servers +-- -------------------------------------------------------------------------- +-- result is in order of preference, including a round robin style random +-- selection of the preference of hosts with the same preference +-- +-- returns: +-- hostlist: flattened list of LDAP servers in order of preference + +function ldap.get_hosts() + + math.randomseed(os.time()) + + local hostlist = {} + + -- iterate through all entries on base level + for i, host in ipairs (config.ldap.hosts) do + + -- a single host entry + if host.uri then + hostlist[#hostlist+1] = host + + -- or a list of host entries + else + local subhostlist = {} + + -- sort list of host entries by random + for j, subhost in ipairs(host) do + subhost.priority = math.random() + subhostlist[#subhostlist+1] = subhost + end + table.sort(subhostlist, function (a,b) + return a.priority < b.priority + end) + + -- and add them to the main list + for i, subhost in ipairs(subhostlist) do + hostlist[#hostlist+1] = subhost + end + + end + + end + + return hostlist + +end diff -r effce9b361b2 -r 58f48a8a202a env/ldap/get_member_entry.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/ldap/get_member_entry.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,52 @@ +-- gets the corresponding ldap entry for a given member login +-- -------------------------------------------------------------------------- +-- +-- arguments: +-- filter: the LDAP filter for searching the member (required) +-- use_ldap_conn: an already existing LDAP connection to be used (optional) +-- +-- returns: +-- ldap_entry: in case of success, the LDAP entry (object) +-- err: in case of an error, an error message (string) +-- err2: error dependent extra error information + +function ldap.get_member_entry(filter, use_ldap_conn) + + local ldap_conn, err + + if use_ldap_conn then + ldap_conn = use_ldap_conn + else + ldap_conn, bind_err = ldap.bind_as_app() + end + + if not ldap_conn then + return nil, "ldap_bind_error", bind_err + end + + local entries, search_err = ldap_conn:search{ + base = config.ldap.base, + scope = config.ldap.member.scope, + filter = filter, + attr = config.ldap.member.fetch_attr, + } + + if not use_ldap_conn then + ldap_conn:unbind() + end + + if not entries then + return nil, "ldap_search_error", search_err + end + + if #entries > 1 then + return nil, "too_many_ldap_entries_found" + end + + if #entries < 0 then + return nil, "no_ldap_entry_found" + end + + return entries[1] + +end diff -r effce9b361b2 -r 58f48a8a202a env/ldap/test.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/ldap/test.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,141 @@ +-- Part of test case for ldap4lqfb + +function ldap.test() + + local global_count = 0 + local global_failed = 0 + + local function checkMember(uid, login, name, units_with_voting_priv, units_with_polling_priv) + + local failed = false + + local function testError(err, expected, got) + failed = true + global_failed = global_failed + 1 + if expected then + print ("[" .. uid .. "] FAILED: " .. err .. " (expected: '" .. expected .. "' but got '" .. tostring(got) .. "')") + else + print ("[" .. uid .. "] FAILED: " .. err) + end + end + + local function testOk(test, expected) + if test then + print ("[" .. uid .. "] " .. test .. " ok ('" .. expected .. "')") + else + print ("[" .. uid .. "] success") + end + end + + local function test(field, expected, value) + global_count = global_count + 1 + if expected ~= value then + testError(field, expected, value) + return + end + testOk(field, expected) + end + + local members = Member:new_selector() + :add_field{ "authority_data->'login' as authority_data_login" } + :add_where{ "authority = ? AND authority_data->'uid' = ?", "ldap", uid } + :exec() + + if #members < 1 then + testError("Member not found in DB") + return + end + + if #members > 1 then + testError("Found more than one DB entry") + return + end + + local member = members[1] + + if login == nil then + if not member.locked then + testError("Member not locked") + else + testOk("Member is locked", "true") + end + return + end + + test("login", login, member.authority_data_login) + test("name", name, member.name) + + for i, unit_id in ipairs(units_with_voting_priv) do + global_count = global_count + 1 + local privilege = Privilege:by_pk(unit_id, member.id) + if privilege and privilege.voting_right then + testOk("voting_right", unit_id) + else + testError("voting_right", unit_id, "") + end + end + + local privileges_selector = Privilege:new_selector() + :add_where{ "member_id = ?", member.id } + :add_where("voting_right = true") + if #units_with_voting_priv > 0 then + local privileges = privileges_selector:add_where{ "unit_id NOT IN ($)", units_with_voting_priv } + end + local privileges = privileges_selector:exec() + + if #privileges > 0 then + testError("voting_right", "count: 0", "count: " .. #privileges) + else + testOk("voting_right", "count: 0") + end + + for i, unit_id in ipairs(units_with_polling_priv) do + global_count = global_count + 1 + local privilege = Privilege:by_pk(unit_id, member.id) + if privilege and privilege.polling_right then + testOk("polling_right", unit_id) + else + testError("polling_right", unit_id, "") + end + end + + local privileges_selector = Privilege:new_selector() + :add_where{ "member_id = ?", member.id } + :add_where("polling_right = true") + if #units_with_polling_priv > 0 then + privileges_selector:add_where{ "unit_id NOT IN ($)", units_with_polling_priv } + end + local privileges = privileges_selector:exec() + + if #privileges > 0 then + testError("polling_right", "count: " .. #units_with_polling_priv, "count: " .. #privileges) + else + testOk("polling_right", "count: " .. #units_with_polling_priv) + end + + if not failed then + return true + end + + return false + + end + + checkMember("1000", "alice", "Alice Amberg", { 1 }, { }) + checkMember("1001", "bob", "Bob Bobbersen", { 1, 2 }, { 1, 3 }) + checkMember("1002", "chris", "Chris Carv", { 1, 2 }, { 1, 3 }) + checkMember("1003", "daisy", "Daisy Duck", { 3 }, { 1, 2 }) + checkMember("1004", "ernst", "Ernst Ernst", { 3 }, { 1, 2 }) + checkMember("1005", "fredi", "Frederike Frei", { 1 }, { }) + checkMember("1006") + checkMember("1007", "helen", "Helen Hofstatter", { 1, 2 }, { 1, 3 }) + checkMember("1008", "iwan", "Iwan Iwanowski", { 1 }, { }) + checkMember("1009", "jasmina", "Jasmina Jasik", { 1 }, { }) + + print() + + local success_count = global_count - global_failed + + print (success_count .. " of " .. global_count .. " tests succeeded.") + +end diff -r effce9b361b2 -r 58f48a8a202a env/ldap/update_all_members.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/ldap/update_all_members.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,68 @@ +-- check for all LiquidFeedback Members with LDAP authentication +-- if the corresponding LDAP entry is still existent and updates +-- changed attributes +-- -------------------------------------------------------------------------- +-- prints debug output to stdout +-- +-- returns +-- success: true if no error occured during run +-- false if at least one error occured during run + +function ldap.update_all_members() + + local some_error_occured = false + + local ldap_conn = ldap.bind_as_app() + + function update_member(member) + + local function failure (err, err2) + Member.get_db_conn():query("ROLLBACK") + io.stdout:write("ERROR: ", err, " (", err2, ") id=", tostring(member.id), " uid=", tostring(member.authority_data_uid), "\n") + some_error_occured = true + end + + local function success () + Member.get_db_conn():query("COMMIT") + io.stdout:write("ok: id=", tostring(member.id), " uid=", tostring(member.authority_data_uid), "\n") + end + + Member.get_db_conn():query("BEGIN") + + local ldap_conn, ldap_entry, err, err2 = ldap.update_member_attr(member, ldap_conn) + if err then + failure("ldap_update_member", err) + return + end + + local err = member:try_save() + if err then + failure("member_try_save", err) + return + end + + if ldap_entry then + local success, err, err2 = ldap.update_member_privileges(member, ldap_entry) + if err then + failure("ldap_update_member_privileges", err) + return + end + + end + + success() + + end + + + local members = Member:get_all_by_authority("ldap") + + for i, member in ipairs(members) do + update_member(member) + end + + ldap_conn:unbind() + + return not some_error_occured + +end diff -r effce9b361b2 -r 58f48a8a202a env/ldap/update_member_attr.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/ldap/update_member_attr.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,52 @@ +-- check if the corresponding LDAP entry for an LiquidFeedback member +-- object is still existent and updates changed attributes +-- -------------------------------------------------------------------------- +-- +-- arguments: +-- member: a LiquidFeedback Member object (required) +-- ldap_conn: a ldap connection handle (optional) +-- uid: the uid of the member (optional, required when creating members) +-- +-- returns: +-- ldap_conn: an LDAP connection +-- ldap_entry: the found LDAP entry (if any) +-- err: error code in case of an error (string) +-- err2: error dependent extra error information +-- err3: error dependent extra error information + +function ldap.update_member_attr(member, ldap_conn, uid) + + -- do this only for members with ldap authentication + if member.authority ~= "ldap" then + return nil, nil, "member_is_not_authenticated_by_ldap" + end + + local filter = config.ldap.member.uid_filter_map(member.authority_data_uid or uid) + local ldap_entry, err, err2 = ldap.get_member_entry(filter, ldap_conn) + + if err then + return ldap_conn, nil, "ldap_error", err, err2 + end + + -- If no corresponding entry found, lock the member + if not ldap_entry then + member.locked = true + member.active = false + return ldap_conn + end + + -- If exactly one corresponding entry found, update the attributes + local err = config.ldap.member.attr_map(ldap_entry, member) + + member.authority_data = encode.pg_hstore{ + uid = member.authority_data_uid or uid, + login = config.ldap.member.login_map(ldap_entry) + } + + if err then + return ldap_conn, ldap_entry, "attr_map_error", err + end + + return ldap_conn, ldap_entry + +end diff -r effce9b361b2 -r 58f48a8a202a env/ldap/update_member_privileges.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/ldap/update_member_privileges.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,56 @@ +-- Update member privileges from LDAP +-- -------------------------------------------------------------------------- +-- +-- arguments: +-- member: the member for which the privileges should be updated +-- ldap_entry: the ldap entry to be used for updating the privileges +-- +-- returns: +-- err: an error code, if an error occured (string) +-- err2: Error dependent extra error information + +function ldap.update_member_privileges(member, ldap_entry) + + local privileges, err = config.ldap.member.privilege_map(ldap_entry, member) + + if err then + return false, "privilege_map_error", err + end + + local privileges_by_unit_id = {} + for i, privilege in ipairs(privileges) do + privileges_by_unit_id[privilege.unit_id] = privilege + end + + local current_privileges = Privilege:by_member_id(member.id) + local current_privilege_ids = {} + + for i, privilege in ipairs(current_privileges) do + if privileges_by_unit_id[privilege.unit_id] then + current_privilege_ids[privilege.unit_id] = privilege + else + privilege:destroy() + end + end + + for i, privilege in ipairs(privileges) do + local current_privilege = current_privilege_ids[privilege.unit_id] + if not current_privilege then + current_privilege = Privilege:new() + current_privilege.member_id = member.id + current_privileges[#current_privileges+1] = current_privilege + end + for key, val in pairs(privilege) do + current_privilege[key] = val + end + end + + for i, privilege in ipairs(current_privileges) do + local err = privilege:try_save() + if err then + return false, "privilege_save_error", err + end + end + + return true +end diff -r effce9b361b2 -r 58f48a8a202a env/util/is_profile_field_locked.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/util/is_profile_field_locked.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,16 @@ +function util.is_profile_field_locked(member, field_name) + if member.authority == "ldap" then + if config.ldap.member.locked_profile_fields and config.ldap.member.locked_profile_fields[field_name] + or field_name == "login" + or field_name == "password" + then + return true + end + end + + if config.locked_profile_fields[field_name] then + return true + end + + return false +end \ No newline at end of file diff -r effce9b361b2 -r 58f48a8a202a lib/mldap/Makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mldap/Makefile Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,8 @@ +mldap.so: mldap.o + ld -shared -L/usr/lib -o mldap.so mldap.o /usr/lib/libldap.so + +mldap.o: mldap.c + gcc -g -c -fPIC -I/usr/include -I/usr/include/lua5.1 -Wall -o mldap.o mldap.c + +clean:: + rm -f mldap.so mldap.o diff -r effce9b361b2 -r 58f48a8a202a lib/mldap/convert_errorcodes.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mldap/convert_errorcodes.lua Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,7 @@ +#!/usr/bin/env lua +for line in io.lines() do + local ident, code = line:match("^#define[ \t]+LDAP_([A-Z][A-Z_]*)[ \t]+([^ \t/]+)") + if ident then + io.stdout:write(' {"', ident:lower(), '", ', tonumber(code) or code, '},\n') + end +end diff -r effce9b361b2 -r 58f48a8a202a lib/mldap/mldap.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mldap/mldap.c Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,567 @@ +/* + * minimalistic Lua LDAP interface library + * + * The library does not set any global variable itself and must thus be + * loaded with + * + * mldap = require("mldap") + * + * or a similar statement. + * + * The library provides two functions, conn = bind{...} and unbind(conn) + * to connect to and disconnect from the LDAP server respectively. The + * unbind function is also provided as a method of the connection userdata + * object (see below). + * + * The arguments to the bind{...} function are documented in the source code + * (see C function mldap_bind). In case of success, the bind function returns + * a userdata object that provides two methods: query{...} and unbind(). In + * case of error, the bind function returns nil as first return value, an + * error string as second return value, and a numeric error code as third + * return value. A positive error code is an LDAP resultCode, a negative + * error code is an OpenLDAP API error code: + * + * connection, error_string, numeric_error_code = mldap.bind{...} + * + * For translating numeric error codes to an identifying (machine readable) + * string identifier, the library provides in addition to the two functions + * a table named 'errorcodes', for example: + * + * 49 == mldap.errorcodes["invalid_credentials"] + * + * and + * + * mldap.errorcodes[49] == "invalid_credentials" + * + * The arguments and return value of the query{...} method of the connection + * userdata object are also documented in the source code below (see + * C function mldap_query). Error conditions are reported the same way as the + * bind{...} function does. + * + * To close the connection, either the unbind() function of the library or + * the unbind() method can be called; it is allowed to call them multiple + * times, and they are also invoked by the garbage collector. + * + */ + +// Lua header inclusions: +#include +#include + +// OpenLDAP header inclusions: +#include + +// Standard C inclusions: +#include +#include +#include + +// Error code translation is included from separate C file: +#include "mldap_errorcodes.c" + +// Provide compatibility with Lua 5.1: +#if LUA_VERSION_NUM < 502 +#define luaL_newlib(L, t) lua_newtable((L)); luaL_register(L, NULL, t) +#define lua_rawlen lua_objlen +#define lua_len lua_objlen +#define luaL_setmetatable(L, regkey) \ + lua_getfield((L), LUA_REGISTRYINDEX, (regkey)); \ + lua_setmetatable((L), -2); +#endif + +// prefix for all Lua registry entries of this library: +#define MLDAP_REGKEY "556aeaf3c864af2e_mldap_" + + +static const char *mldap_get_named_string_arg( + // gets a named argument of type "string" from a table at the given stack position + + lua_State *L, // pointer to lua_State variable + int idx, // stack index of the table containing the named arguments + const char *argname, // name of the argument + int mandatory // if not 0, then the argument is mandatory and an error is raised if it isn't found + + // leaves the string as new element on top of the stack +) { + + // pushes the table entry with the given argument name on top of the stack: + lua_getfield(L, idx, argname); + + // check, if the entry is nil or false: + if (!lua_toboolean(L, -1)) { + + // throw error, if named argument is mandatory: + if (mandatory) return luaL_error(L, "Named argument '%s' missing", argname), NULL; + + // return NULL pointer, if named argument is not mandatory: + return NULL; + + } + + // throw error, if the value of the argument is not string: + if (lua_type(L, -1) != LUA_TSTRING) return luaL_error(L, "Named argument '%s' is not a string", argname), NULL; + + // return a pointer to the string, leaving the string on top of the stack to avoid garbage collection: + return lua_tostring(L, -1); + + // leaves one element on the stack +} + + +static int mldap_get_named_number_arg( + // gets a named argument of type "number" from a table at the given stack position + + lua_State *L, // pointer to lua_State variable + int idx, // stack index of the table containing the named arguments + const char *argname, // name of the argument + int mandatory, // if not 0, then the argument is mandatory and an error is raised if it isn't found + lua_Number default_value // default value to return, if the argument is not mandatory and nil or false + + // opposed to 'mldap_get_named_string_arg', this function leaves no element on the stack +) { + + lua_Number value; // value to return + + // pushes the table entry with the given argument name on top of the stack: + lua_getfield(L, idx, argname); + + // check, if the entry is nil or false: + if (lua_isnil(L, -1)) { + + // throw error, if named argument is mandatory: + if (mandatory) return luaL_error(L, "Named argument '%s' missing", argname), 0; + + // set default value as return value, if named argument is not mandatory: + value = default_value; + + } else { + + // throw error, if the value of the argument is not a number: + if (lua_type(L, -1) != LUA_TNUMBER) return luaL_error(L, "Named argument '%s' is not a number", argname), 0; + + // set return value to the number: + value = lua_tonumber(L, -1); + + } + + // remove unnecessary element from stack (not needed to avoid garbage collection): + return value; + + // leaves no new elements on the stack +} + + +static int mldap_scope( + // converts a string ("base", "onelevel", "subtree", "children") to an integer representing the LDAP scope + // and throws an error for any unknown string + + lua_State *L, // pointer to lua_State variable (needed to throw errors) + const char *scope_string // string that is either ("base", "onelevel", "subtree", "children") + + // does not affect or use the Lua stack at all +) { + + // return integer according to string value: + if (!strcmp(scope_string, "base")) return LDAP_SCOPE_BASE; + if (!strcmp(scope_string, "onelevel")) return LDAP_SCOPE_ONELEVEL; + if (!strcmp(scope_string, "subtree")) return LDAP_SCOPE_SUBTREE; + if (!strcmp(scope_string, "children")) return LDAP_SCOPE_CHILDREN; + + // throw error for unknown string values: + return luaL_error(L, "Invalid LDAP scope: '%s'", scope_string), 0; + +} + + +static int mldap_bind(lua_State *L) { + // Lua C function that takes named arguments as a table + // and returns a userdata object, representing the LDAP connection + // or returns nil, an error string, and an error code (integer) on error + + // named arguments: + // "uri" (string) server URI to connect to + // "who" (string) DN to bind as + // "password" (string) password for DN to bind as + // "timeout" (number) timeout in seconds + + static const int ldap_version = LDAP_VERSION3; // providing a pointer (&ldap_version) to set LDAP protocol version 3 + const char *uri; // C string for "uri" argument + const char *who; // C string for "who" argument + struct berval cred; // credentials ("password") are stored as struct berval + lua_Number timeout_float; // float (lua_Number) for timeout + struct timeval timeout; // timeout is stored as struct timeval + int ldap_error; // LDAP error code (as returned by libldap calls) + LDAP *ldp; // pointer to an opaque OpenLDAP structure representing the connection + LDAP **ldp_ptr; // pointer to a Lua userdata structure (that only contains the 'ldp' pointer) + + // throw error if first argument is not a table: + if (lua_type(L, 1) != LUA_TTABLE) { + luaL_error(L, "Argument to function 'bind' is not a table."); + } + + // extract arguments: + uri = mldap_get_named_string_arg(L, 1, "uri", true); + who = mldap_get_named_string_arg(L, 1, "who", false); + cred.bv_val = mldap_get_named_string_arg(L, 1, "password", false); + if (cred.bv_val) cred.bv_len = strlen(cred.bv_val); + else cred.bv_len = 0; + timeout_float = mldap_get_named_number_arg(L, 1, "timeout", false, -1); + timeout.tv_sec = timeout_float; + timeout.tv_usec = (timeout_float - timeout.tv_sec) * 1000000; + + // initialize OpenLDAP structure and provide URI for connection: + ldap_error = ldap_initialize(&ldp, uri); + // on error, jump to label "mldap_queryconn_error1", as no ldap_unbind_ext_s() is needed: + if (ldap_error != LDAP_SUCCESS) goto mldap_queryconn_error1; + + // set LDAP protocol version 3: + ldap_error = ldap_set_option(ldp, LDAP_OPT_PROTOCOL_VERSION, &ldap_version); + // on error, jump to label "mldap_queryconn_error2", as ldap_unbind_ext_s() must be called: + if (ldap_error != LDAP_SUCCESS) goto mldap_queryconn_error2; + + // set timeout for asynchronous OpenLDAP library calls: + ldap_error = ldap_set_option(ldp, LDAP_OPT_TIMEOUT, &timeout); + // on error, jump to label "mldap_queryconn_error2", as ldap_unbind_ext_s() must be called: + if (ldap_error != LDAP_SUCCESS) goto mldap_queryconn_error2; + + // connect to LDAP server: + ldap_error = ldap_sasl_bind_s( + ldp, // pointer to opaque OpenLDAP structure representing the connection + who, // DN to bind as + LDAP_SASL_SIMPLE, // SASL mechanism "simple" for password authentication + &cred, // password as struct berval + NULL, // no server controls + NULL, // no client controls + NULL // do not store server credentials + ); + + // error handling: + if (ldap_error != LDAP_SUCCESS) { + + // error label to jump to, if a call of ldap_unbind_ext_s() is required: + mldap_queryconn_error2: + + // close connection and free resources: + ldap_unbind_ext_s(ldp, NULL, NULL); + + // error label to jump to, if no call of ldap_unbind_ext_s() is required: + mldap_queryconn_error1: + + // return three values: + lua_pushnil(L); // return nil as first value + lua_pushstring(L, ldap_err2string(ldap_error)); // return error string as second value + lua_pushinteger(L, ldap_error); // return error code (integer) as third value + return 3; + + } + + // create new Lua userdata object (that will contain the 'ldp' pointer) on top of stack: + ldp_ptr = lua_newuserdata(L, sizeof(LDAP *)); + + // set metatable of Lua userdata object: + luaL_setmetatable(L, MLDAP_REGKEY "connection_metatable"); + + // write contents of Lua userdata object (the 'ldp' pointer): + *ldp_ptr = ldp; + + // return Lua userdata object from top of stack: + return 1; + +} + + +static int mldap_search(lua_State *L) { + // Lua C function used as "search" method of Lua userdata object representing the LDAP connection + // that takes a Lua userdata object (the LDAP connection) as first argument, + // a table with named arguments as second argument, + // and returns a result table on success (see below) + // or returns nil, an error string, and an error code (integer) on error + + // named arguments: + // "base" (string) DN of the entry at which to start the search + // "scope" (string) scope of the search, one of: + // "base" to search the object itself + // "onelevel" to search the object's immediate children + // "subtree" to search the object and all its descendants + // "children" to search all of the descendants + // "filter" (string) string representation of the filter to apply in the search + // (conforming to RFC 4515 as extended in RFC 4526) + // "attrs" (table) list of attribute descriptions (each a string) to return from matching entries + + // structure of result table: + // { + // { dn = , + // = { , , ... }, + // = { , , ... }, + // ... + // }, + // { dn = , + // = { , , ... }, + // = { , , ... }, + // ... + // }, + // ... + // } + + const char *base; // C string for "base" argument + const char *scope_string; // C string for "scope" argument + int scope; // integer representing the scope + const char *filter; // C string for "filter" argument + size_t nattrs; // number of attributes in "attrs" argument + char **attrs; // C string array of "attrs" argument + size_t attr_idx; // index variable for building the C string array of "attrs" + int ldap_error; // LDAP error code (as returned by libldap calls) + LDAP **ldp_ptr; // pointer to a pointer to the OpenLDAP structure representing the connection + LDAPMessage *res; // pointer to the result of ldap_search_ext_s() call + LDAPMessage *ent; // pointer to an entry in the result of ldap_search_ext_s() call + int i; // integer to fill the Lua table returned as result + + // truncate the Lua stack to 2 elements: + lua_settop(L, 2); + + // check if the first argument is a Lua userdata object with the correct metatable + // and get a C pointer to that userdata object: + ldp_ptr = luaL_checkudata(L, 1, MLDAP_REGKEY "connection_metatable"); + + // throw an error, if the connection has already been closed: + if (!*ldp_ptr) { + return luaL_error(L, "LDAP connection has already been closed"); + } + + // check if the second argument is a table, and throw an error if it's not a table: + if (lua_type(L, 2) != LUA_TTABLE) { + luaL_error(L, "Argument to function 'bind' is not a table."); + } + + // extract named arguments (requires memory allocation for 'attrs'): + base = mldap_get_named_string_arg(L, 2, "base", true); // pushed to 3 + scope_string = mldap_get_named_string_arg(L, 2, "scope", true); // pushed to 4 + scope = mldap_scope(L, scope_string); + lua_pop(L, 1); // removes stack element 4 + filter = mldap_get_named_string_arg(L, 2, "filter", false); // pushed to 4 + lua_getfield(L, 2, "attrs"); // pushed to 5 + nattrs = lua_len(L, -1); + attrs = calloc(nattrs + 1, sizeof(char *)); // memory allocation, +1 for terminating NULL + if (!attrs) return luaL_error(L, "Memory allocation error in C function 'mldap_queryconn'"); + for (attr_idx=0; attr_idxbv_val, (*val)->bv_len); + + // pop value from Lua stack position 5 + // and store it in table on Lua stack position 4: + lua_rawseti(L, 4, j); + + } + + // free data structure of values: + ldap_value_free_len(vals); + + // pop attribute name from Lua stack position 3 + // and pop value table from Lua stack position 4 + // and store them in result table for entry at Lua stack position 2: + lua_settable(L, 2); + + } + + // free 'BerElement *ber' stucture that has been used to iterate through all attributes + // (second argument is zero due to manpage of ldap_first_attribute()): + ber_free(ber, 0); + + // store distinguished name (DN) on Lua stack position 3 + // (aquired memory is free'd with ldap_memfree()): + dn = ldap_get_dn(*ldp_ptr, ent); + lua_pushstring(L, dn); + ldap_memfree(dn); + + // pop distinguished name (DN) from Lua stack position 3 + // and store it in field "dn" of entry result table at stack position 2 + lua_setfield(L, 2, "dn"); + + // pop entry result table from Lua stack position 2 + // and store it in table at stack position 1: + lua_rawseti(L, 1, i); + + } + + // return result table from top of Lua stack (stack position 1): + return 1; + +} + +static int mldap_unbind(lua_State *L) { + // Lua C function used as "unbind" function of module and "unbind" method of Lua userdata object + // closing the LDAP connection (if still open) + // returning nothing + + LDAP **ldp_ptr; // pointer to a pointer to the OpenLDAP structure representing the connection + + // check if the first argument is a Lua userdata object with the correct metatable + // and get a C pointer to that userdata object: + ldp_ptr = luaL_checkudata(L, 1, MLDAP_REGKEY "connection_metatable"); + + // check if the Lua userdata object still contains a pointer: + if (*ldp_ptr) { + + // if it does, then call ldap_unbind_ext_s(): + ldap_unbind_ext_s( + *ldp_ptr, // pointer to the opaque OpenLDAP structure representing the connection + NULL, // no server controls + NULL // no client controls + ); + + // store NULL pointer in Lua userdata to mark connection as closed + *ldp_ptr = NULL; + } + + // returning nothing: + return 0; + +} + + +// registration information for library functions: +static const struct luaL_Reg mldap_module_functions[] = { + {"bind", mldap_bind}, + {"unbind", mldap_unbind}, + {NULL, NULL} +}; + + +// registration information for methods of connection object: +static const struct luaL_Reg mldap_connection_methods[] = { + {"search", mldap_search}, + {"unbind", mldap_unbind}, + {NULL, NULL} +}; + + +// registration information for connection metatable: +static const struct luaL_Reg mldap_connection_metamethods[] = { + {"__gc", mldap_unbind}, + {NULL, NULL} +}; + + +// luaopen function to initialize/register library: +int luaopen_mldap(lua_State *L) { + + // clear Lua stack: + lua_settop(L, 0); + + // create table with library functions on Lua stack position 1: + luaL_newlib(L, mldap_module_functions); + + // create metatable for connection objects on Lua stack position 2: + luaL_newlib(L, mldap_connection_metamethods); + + // create table with methods for connection object on Lua stack position 3: + luaL_newlib(L, mldap_connection_methods); + + // pop table with methods for connection object from Lua stack position 3 + // and store it as "__index" in metatable: + lua_setfield(L, 2, "__index"); + + // pop table with metatable for connection objects from Lua stack position 2 + // and store it in the Lua registry: + lua_setfield(L, LUA_REGISTRYINDEX, MLDAP_REGKEY "connection_metatable"); + + // create table for error code mappings on Lua stack position 2: + lua_newtable(L); + + // fill table for error code mappings + // that maps integer error codes to error code strings + // and vice versa: + mldap_set_errorcodes(L); + + // pop table for error code mappings from Lua stack position 2 + // and store it as "errorcodes" in table with library functions: + lua_setfield(L, 1, "errorcodes"); + + // return table with library functions from top of Lua stack: + return 1; + +} diff -r effce9b361b2 -r 58f48a8a202a lib/mldap/mldap_errorcodes.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mldap/mldap_errorcodes.c Fri Jul 18 21:42:59 2014 +0200 @@ -0,0 +1,199 @@ +/* This file contains error code mappings of LDAP error codes and OpenLDAP + * error codes. + * + * The collection of error codes (mldap_errorcodes[]) has been derived from + * the file ldap.h that is part of OpenLDAP Software. OpenLDAP's license + * information is stated below: + * + * This work is part of OpenLDAP Software . + * + * Copyright 1998-2013 The OpenLDAP Foundation. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted only as authorized by the OpenLDAP + * Public License. + * + * A copy of this license is available below: + * + * The OpenLDAP Public License + * Version 2.8, 17 August 2003 + * + * Redistribution and use of this software and associated documentation + * ("Software"), with or without modification, are permitted provided + * that the following conditions are met: + * + * 1. Redistributions in source form must retain copyright statements + * and notices, + * + * 2. Redistributions in binary form must reproduce applicable copyright + * statements and notices, this list of conditions, and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution, and + * + * 3. Redistributions must contain a verbatim copy of this document. + * + * The OpenLDAP Foundation may revise this license from time to time. + * Each revision is distinguished by a version number. You may use + * this Software under terms of this license revision or under the + * terms of any subsequent revision of the license. + * + * THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND ITS + * CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE OPENLDAP FOUNDATION, ITS CONTRIBUTORS, OR THE AUTHOR(S) + * OR OWNER(S) OF THE SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * The names of the authors and copyright holders must not be used in + * advertising or otherwise to promote the sale, use or other dealing + * in this Software without specific, written prior permission. Title + * to copyright in this Software shall at all times remain with copyright + * holders. + * + * OpenLDAP is a registered trademark of the OpenLDAP Foundation. + * + * Copyright 1999-2003 The OpenLDAP Foundation, Redwood City, + * California, USA. All Rights Reserved. Permission to copy and + * distribute verbatim copies of this document is granted. + * + * End of OpenLDAP Public License + * + * Portions Copyright (c) 1990 Regents of the University of Michigan. + * All rights reserved. + * + * Redistribution and use in source and binary forms are permitted + * provided that this notice is preserved and that due credit is given + * to the University of Michigan at Ann Arbor. The name of the University + * may not be used to endorse or promote products derived from this + * software without specific prior written permission. This software + * is provided ``as is'' without express or implied warranty. + * + * End of OpenLDAP's license information + */ + +// type for entry in mldap_errorcodes[] array: +struct mldap_errorcode { + const char *ident; + int code; +}; + +// NULL terminated array of error code strings with error code integers +// derived from ldap.h (see above copyright notice): +static const struct mldap_errorcode mldap_errorcodes[] = { + {"operations_error", 1}, + {"protocol_error", 2}, + {"timelimit_exceeded", 3}, + {"sizelimit_exceeded", 4}, + {"compare_false", 5}, + {"compare_true", 6}, +// {"auth_method_not_supported", 7}, + {"strong_auth_not_supported", 7}, + {"strong_auth_required", 8}, +// {"stronger_auth_required", 8}, + {"partial_results", 9}, + {"referral", 10}, + {"adminlimit_exceeded", 11}, + {"unavailable_critical_extension", 12}, + {"confidentiality_required", 13}, + {"sasl_bind_in_progress", 14}, + {"no_such_attribute", 16}, + {"undefined_type", 17}, + {"inappropriate_matching", 18}, + {"constraint_violation", 19}, + {"type_or_value_exists", 20}, + {"invalid_syntax", 21}, + {"no_such_object", 32}, + {"alias_problem", 33}, + {"invalid_dn_syntax", 34}, + {"is_leaf", 35}, + {"alias_deref_problem", 36}, + {"x_proxy_authz_failure", 47}, + {"inappropriate_auth", 48}, + {"invalid_credentials", 49}, + {"insufficient_access", 50}, + {"busy", 51}, + {"unavailable", 52}, + {"unwilling_to_perform", 53}, + {"loop_detect", 54}, + {"naming_violation", 64}, + {"object_class_violation", 65}, + {"not_allowed_on_nonleaf", 66}, + {"not_allowed_on_rdn", 67}, + {"already_exists", 68}, + {"no_object_class_mods", 69}, + {"results_too_large", 70}, + {"affects_multiple_dsas", 71}, + {"vlv_error", 76}, + {"other", 80}, + {"cup_resources_exhausted", 113}, + {"cup_security_violation", 114}, + {"cup_invalid_data", 115}, + {"cup_unsupported_scheme", 116}, + {"cup_reload_required", 117}, + {"cancelled", 118}, + {"no_such_operation", 119}, + {"too_late", 120}, + {"cannot_cancel", 121}, + {"assertion_failed", 122}, + {"proxied_authorization_denied", 123}, + {"sync_refresh_required", 4096}, + {"x_sync_refresh_required", 16640}, + {"x_assertion_failed", 16655}, + {"x_no_operation", 16654}, + {"x_no_referrals_found", 16656}, + {"x_cannot_chain", 16657}, + {"x_invalidreference", 16658}, + {"x_txn_specify_okay", 16672}, + {"x_txn_id_invalid", 16673}, + {"server_down", (-1)}, + {"local_error", (-2)}, + {"encoding_error", (-3)}, + {"decoding_error", (-4)}, + {"timeout", (-5)}, + {"auth_unknown", (-6)}, + {"filter_error", (-7)}, + {"user_cancelled", (-8)}, + {"param_error", (-9)}, + {"no_memory", (-10)}, + {"connect_error", (-11)}, + {"not_supported", (-12)}, + {"control_not_found", (-13)}, + {"no_results_returned", (-14)}, + {"more_results_to_return", (-15)}, + {"client_loop", (-16)}, + {"referral_limit_exceeded", (-17)}, + {"x_connecting", (-18)}, + {NULL, 0} +}; + +void mldap_set_errorcodes(lua_State *L) { + // stores mldap_errorcodes[] mappings in the Lua table on top of the stack + // in both directions (string mapped to integer and vice versa) + + const struct mldap_errorcode *errorcode; // pointer to entry in mldap_errorcodes[] array + + // iterate through entries in mldap_errorcodes[] array: + for (errorcode=mldap_errorcodes; errorcode->ident; errorcode++) { + + // store a mapping from the string to the integer: + lua_pushstring(L, errorcode->ident); + lua_pushinteger(L, errorcode->code); + lua_settable(L, -3); + + // store a mapping from the integer to the string: + lua_pushinteger(L, errorcode->code); + lua_pushstring(L, errorcode->ident); + lua_settable(L, -3); + + } + +} + diff -r effce9b361b2 -r 58f48a8a202a model/member.lua --- a/model/member.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/model/member.lua Fri Jul 18 21:42:59 2014 +0200 @@ -273,6 +273,17 @@ self.get_db_conn().query("LOCK TABLE " .. self:get_qualified_table() .. " IN ROW SHARE MODE") end + +function Member:get_all_by_authority(authority) + + local members = Member:new_selector() + :add_where{ "authority = ?", authority } + :add_field("authority_data->'uid' as authority_data_uid") + :exec() + + return members +end + function Member.object:set_password(password) trace.disable() @@ -372,17 +383,144 @@ end function Member:by_login_and_password(login, password) - local selector = self:new_selector() - selector:add_field({ "now() > COALESCE(last_delegation_check, activated) + ?::interval", config.check_delegations_interval_hard }, "needs_delegation_check_hard") - selector:add_where{'"login" = ?', login } - selector:add_where('NOT "locked"') - selector:optional_object_mode() - local member = selector:exec() - if member and member:check_password(password) then - return member - else - return nil + + local function prepare_login_selector() + local selector = self:new_selector() + selector:add_field({ "now() > COALESCE(last_delegation_check, activated) + ?::interval", config.check_delegations_interval_hard }, "needs_delegation_check_hard") + selector:add_where('NOT "locked"') + selector:optional_object_mode() + return selector + end + + local function do_local_login() + local selector = prepare_login_selector() + selector:add_where{'"login" = ?', login } + local member = selector:exec() + if member and member:check_password(password) then + return member + else + return nil + end end + + if config.ldap.member then + + -- Let's check the users credentials against the LDAP + local ldap_entry, ldap_err = ldap.check_credentials(login, password) + + -- Is the user already registered as member? + local uid + local selector = prepare_login_selector() + + -- Get login name from LDAP entry + if ldap_entry then + uid = config.ldap.member.uid_map(ldap_entry) + selector:add_where{'"authority" = ? AND "authority_data"->\'uid\' = ?', "ldap", uid } + + -- or build it from the login + else + login = config.ldap.member.login_normalizer(login) + selector:add_where{'"authority" = ? AND "authority_data"->\'login\' = ?', "ldap", login } + end + + local member = selector:exec() + -- The member is already registered + if member then + + -- The credentials entered by the user are invalid + if ldap_err == "invalid_credentials" then + + -- Check if the user tried a cached password (which is invalid now) + if config.ldap.member.cache_passwords and member:check_password(password) then + member.password = nil + member:save() + end + + -- Try a regular login + return do_local_login() + + end + + -- The credentials were accepted by the LDAP server and no error occured + if ldap_entry and not ldap_err then + + -- Cache the password (if feature enabled) + if config.ldap.member.cache_passwords and not member:check_password(password) then + member:set_password(password) + end + + -- update the member attributes and privileges from LDAP + local ldap_conn, ldap_err, err, err2 = ldap.update_member_attr(member, nil, uid) + if not err then + local err = member:try_save() + if err then + return nil, "member_save_error", err + end + local succes, err, err2 = ldap.update_member_privileges(member, ldap_entry) + if err then + return nil, "update_member_privileges_error", err, err2 + end + return member + end + + end + + -- Some kind of LDAP error happened, if cached password are enabled, + -- check user credentials against the cache + if config.ldap.member.cache_passwords and member:check_password(password) then + + -- return the successfully logged in member + return member + + end + + -- The member is not registered + elseif config.ldap.member.registration and ldap_entry and not ldap_err then + -- Automatic registration ("auto") + if config.ldap.member.registration == "auto" then + member = Member:new() + member.authority = "ldap" + local ldap_login + if config.ldap.member.cache_passwords then + if config.ldap.member.login_normalizer then + ldap_login = config.ldap.member.login_normalizer(login) + else + ldap_login = login + end + end + -- TODO change this when SQL layers supports hstore + member.authority_data = encode.pg_hstore{ + uid = uid, + login = ldap_login + } + member.activated = "now" + member.last_activity = "now" + if config.ldap.member.cache_passwords then + member:set_password(password) + end + local ldap_conn, ldap_err, err, err2 = ldap.update_member_attr(member, nil, uid) + if not err then + local err = member:try_save() + if err then + return nil, "member_save_error", err + end + local success, err, err2 = ldap.update_member_privileges(member, ldap_entry) + if err then + return nil, "update_member_privileges_error", err, err2 + end + return member + end + + -- No automatic registration + else + return nil, "ldap_credentials_valid_but_no_member", uid + end + end + + end + + return do_local_login() + end function Member:by_login(login) diff -r effce9b361b2 -r 58f48a8a202a model/privilege.lua --- a/model/privilege.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/model/privilege.lua Fri Jul 18 21:42:59 2014 +0200 @@ -23,4 +23,10 @@ :add_where{ "unit_id = ? AND member_id = ?", unit_id, member_id } :optional_object_mode() :exec() +end + +function Privilege:by_member_id(member_id) + return self:new_selector() + :add_where{ "member_id = ?", member_id } + :exec() end \ No newline at end of file diff -r effce9b361b2 -r 58f48a8a202a model/session.lua --- a/model/session.lua Thu Jul 17 23:38:35 2014 +0200 +++ b/model/session.lua Fri Jul 18 21:42:59 2014 +0200 @@ -28,6 +28,7 @@ function Session:by_ident(ident) local selector = self:new_selector() selector:add_where{ 'ident = ?', ident } + selector:add_field{ '"authority_data" -> \'uid\' as authority_data_uid' } selector:optional_object_mode() return selector:exec() end