| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- local json = require "json"
- local auth_url = "http://app:3001/internal/dovecot/auth"
- local secret_file = "/run/secrets/dovecot_auth_secret"
- local readonly_acl_group = "mailhub_webmail_readonly"
- local http_client
- local shared_secret
- local function read_secret(path)
- local file = io.open(path, "r")
- if file == nil then
- error("Dovecot authentication secret is unavailable")
- end
- local value = file:read("*a")
- file:close()
- value = string.gsub(value or "", "^%s+", "")
- value = string.gsub(value, "%s+$", "")
- if #value < 32 or #value > 512 or string.find(value, "%s") ~= nil then
- error("Dovecot authentication secret is invalid")
- end
- return value
- end
- function script_init()
- shared_secret = read_secret(secret_file)
- http_client = dovecot.http.client {
- auto_retry = "no",
- request_max_attempts = 1,
- connect_timeout = "1s",
- request_timeout = "30s",
- request_absolute_timeout = "30s"
- }
- return 0
- end
- local function failure(result)
- return result, nil
- end
- local function valid_user(value)
- if type(value) ~= "string" or #value == 0 or #value > 320 then
- return false
- end
- if string.find(value, "/", 1, true) ~= nil
- or string.find(value, "\\", 1, true) ~= nil
- or string.find(value, "\0", 1, true) ~= nil
- or string.find(value, "%s") ~= nil then
- return false
- end
- return string.find(value, "^[^@]+@[^@]+%.[^@]+$") ~= nil
- end
- local function first_nonempty_string(...)
- for index = 1, select("#", ...) do
- local value = select(index, ...)
- if value ~= nil then
- local normalized = tostring(value)
- if normalized ~= "" then
- return normalized
- end
- end
- end
- return ""
- end
- function auth_password_verify(request, password)
- local protocol = string.lower(first_nonempty_string(request.protocol, request.service))
- local remote_ip = first_nonempty_string(
- request.remote_ip,
- request.real_remote_ip,
- request.rip,
- request.real_rip
- )
- if protocol ~= "imap" and protocol ~= "pop3" then
- return failure(dovecot.auth.PASSDB_RESULT_USER_DISABLED)
- end
- local http_request = http_client:request {
- url = auth_url,
- method = "POST"
- }
- http_request:add_header("content-type", "application/json")
- http_request:add_header("authorization", "Bearer " .. shared_secret)
- http_request:add_header("connection", "close")
- http_request:set_payload(json.encode {
- username = request.user,
- password = password,
- service = protocol,
- remoteIp = remote_ip
- })
- local submitted, response = pcall(function()
- return http_request:submit()
- end)
- if not submitted then
- return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
- end
- local status = response:status()
- if status ~= 200 then
- return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
- end
- local decoded, payload = pcall(json.decode, response:payload())
- if not decoded or type(payload) ~= "table" then
- return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
- end
- if payload.authenticated == false then
- return failure(dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH)
- end
- if payload.authenticated ~= true then
- return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
- end
- if not valid_user(payload.user) then
- return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
- end
- local fields = { user = string.lower(payload.user) }
- if payload.aclGroups ~= nil then
- if payload.aclGroups ~= readonly_acl_group then
- return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
- end
- fields.userdb_acl_groups = readonly_acl_group
- -- A private namespace grants implicit owner rights, including root-level
- -- mailbox creation. Public type removes those implicit rights; the fixed
- -- ACL group below grants back only lookup/read/Seen.
- fields["userdb_namespace/inbox/type"] = "public"
- end
- return dovecot.auth.PASSDB_RESULT_OK, fields
- end
|