view mod_password_policy/mod_password_policy.lua @ 4876:0f5f2d4475b9

mod_http_xep227: Add support for import via APIs rather than direct store manipulation In particular this transitions PEP nodes and data to be imported via mod_pep's APIs, fixing issues with importing at runtime while PEP data may already be live in RAM. Next obvious candidate for this approach is rosters, so clients get immediate roster pushes and other special handling (such as emitting subscribes to reach the desired subscription state).
author Matthew Wild <mwild1@gmail.com>
date Tue, 18 Jan 2022 17:01:18 +0000
parents bfd4af4caddc
children d3b69859553a
line wrap: on
line source

-- Password policy enforcement for Prosody
--
-- Copyright (C) 2012 Waqas Hussain
--
--
-- Configuration:
--    password_policy = {
--        length = 8;
--    }


local it = require "util.iterators";
local set = require "util.set";
local st = require "util.stanza";

local options = module:get_option("password_policy");

options = options or {};
options.length = options.length or 8;
if options.exclude_username == nil then
	options.exclude_username = true;
end

local builtin_policies = set.new({ "length", "exclude_username" });
local extra_policies = set.new(it.to_array(it.keys(options))) - builtin_policies;

local extra_policy_handlers = {};

module:handle_items("password-policy-provider", function (event)
	-- Password policy handler added
	local item = event.item;
	module:log("error", "Adding password policy handler '%s'", item.name);
	extra_policy_handlers[item.name] = item.check_password;
end, function (event)
	-- Password policy handler removed
	local item = event.item;
	extra_policy_handlers[item.name] = nil;
end);

function check_password(password, additional_info)
	if not password or password == "" then
		return nil, "No password provided", "no-password";
	end

	if #password < options.length then
		return nil, ("Password is too short (minimum %d characters)"):format(options.length), "length";
	end

	if additional_info then
		local username = additional_info.username;
		if username and password:lower():find(username:lower(), 1, true) then
			return nil, "Password must not include your username", "username";
		end
	end

	for policy in extra_policies do
		local handler = extra_policy_handlers[policy];
		if not handler then
			module:log("error", "No policy handler found for '%s' (typo, or module not loaded?)", policy);
			return nil, "Internal error while verifying password", "internal";
		end
		local ok, reason_text, reason_name = handler(password, options[policy], additional_info);
		if ok ~= true then
			return nil, reason_text or ("Password failed %s check"):format(policy), reason_name or policy;
		end
	end

	return true;
end

function get_policy() --luacheck: ignore 131/get_policy
	return options;
end

function handler(event)
	local origin, stanza = event.origin, event.stanza;

	if stanza.attr.type == "set" then
		local query = stanza.tags[1];

		local passwords = {};

		local dataform = query:get_child("x", "jabber:x:data");
		if dataform then
			for _,tag in ipairs(dataform.tags) do
				if tag.attr.var == "password" then
					table.insert(passwords, tag:get_child_text("value"));
				end
			end
		end

		table.insert(passwords, query:get_child_text("password"));

		local additional_info = {
			username = origin.username;
		};

		for _,password in ipairs(passwords) do
			if password then
				local pw_ok, pw_err, pw_failed_policy = check_password(password, additional_info);
				if not pw_ok then
					module:log("debug", "Password failed check against '%s' policy", pw_failed_policy);
					origin.send(st.error_reply(stanza, "cancel", "not-acceptable", pw_err));
					return true;
				end
			end
		end
	end
end

module:hook("iq/self/jabber:iq:register:query", handler, 10);
module:hook("iq/host/jabber:iq:register:query", handler, 10);
module:hook("stanza/iq/jabber:iq:register:query", handler, 10);