From 0e0442b0650ceb74175905578054db8877b1bbbd Mon Sep 17 00:00:00 2001 From: Clinton Ebadi Date: Fri, 9 May 2014 04:40:31 -0400 Subject: [PATCH] Add vmail command for changing password when you know the current password Not 100% sure if this the best way, but the members portal was tied to *the* mail node, which is not good to begin with, and breaks when there are multiple mail nodes. * Replaces vmailpasswd.c, which is an awful program (passed password on the command line revealing it to `ps' and only supports a local filesystem userdb). * Restricted to users with the priv `vmail' for now, and only used by the portal. Not much worth in exposing generally it seems (vmail users cannot login to any shell machines, at least at hcoop) * Includes helper python program to run crypt() (better than C at least...) * New function to parse the userdb into a StringMap (a better approach is possible, similar to the Vmail.list). Will be used to compile the database for Dovecot later. * New binary `domtool-portal' to expose replacement vmailpasswd command --- Makefile | 11 +++++- scripts/domtool-vmailpasswd | 27 +++++++++++++++ src/mail/vmail.sig | 3 ++ src/mail/vmail.sml | 69 +++++++++++++++++++++++++++++++++++++ src/main-portal.sml | 44 +++++++++++++++++++++++ src/main.sig | 2 ++ src/main.sml | 36 +++++++++++++++++++ src/msg.sml | 10 ++++++ src/msgTypes.sml | 2 ++ 9 files changed, 203 insertions(+), 1 deletion(-) create mode 100755 scripts/domtool-vmailpasswd create mode 100644 src/main-portal.sml diff --git a/Makefile b/Makefile index 7b85747..89ae22c 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ config.sml: mlton: bin/domtool-server bin/domtool-client bin/domtool-slave \ bin/domtool-admin bin/domtool-doc bin/dbtool bin/vmail \ bin/smtplog bin/setsa bin/mysql-fixperms bin/webbw bin/domtool-tail \ - bin/fwtool bin/domtool-config + bin/fwtool bin/domtool-config bin/domtool-portal smlnj: $(COMMON_DEPS) openssl/smlnj/FFI/libssl.h.cm pcre/smlnj/FFI/libpcre.h.cm \ src/domtool.cm @@ -125,6 +125,10 @@ src/domtool-config.mlb: src/prefix.mlb src/sources src/suffix.mlb $(MAKE_MLB_BASE) >src/domtool-config.mlb echo "main-config.sml" >>src/domtool-config.mlb +src/domtool-portal.mlb: src/prefix.mlb src/sources src/suffix.mlb + $(MAKE_MLB_BASE) >src/domtool-portal.mlb + echo "main-portal.sml" >>src/domtool-portal.mlb + openssl/smlnj/FFI/libssl.h.cm: openssl/openssl_sml.h cd openssl/smlnj ; ml-nlffigen -d FFI -lh LibsslH.libh -include ../libssl-h.sml \ -cm libssl.h.cm -D__builtin_va_list="void*" \ @@ -215,6 +219,9 @@ bin/domtool-tail: $(COMMON_MLTON_DEPS) src/tail/tail.mlb src/tail/*.sml bin/domtool-config: $(COMMON_MLTON_DEPS) src/domtool-config.mlb src/main-config.sml $(MLTON) -output bin/domtool-config src/domtool-config.mlb +bin/domtool-portal: $(COMMON_MLTON_DEPS) src/domtool-portal.mlb src/main-portal.sml + $(MLTON) -output bin/domtool-portal src/domtool-portal.mlb + elisp/domtool-tables.el: lib/*.dtl bin/domtool-doc bin/domtool-doc -basis -emacs >$@ @@ -226,6 +233,7 @@ install: install_sos cp scripts/domtool-publish /usr/local/sbin/ cp scripts/domtool-reset-global /usr/local/sbin/ cp scripts/domtool-reset-local /usr/local/sbin/ + cp scripts/domtool-vmailpasswd /usr/local/sbin/ cp scripts/domtool-adduser /usr/local/bin/ cp scripts/domtool-addcert /usr/local/bin/ cp scripts/domtool-readdcerts /usr/local/bin/ @@ -252,6 +260,7 @@ install: install_sos -cp bin/domtool-tail /usr/local/bin/ -chmod +s /usr/local/bin/domtool-tail cp bin/domtool-config /usr/local/bin/ + cp bin/domtool-portal /usr/local/sbin/ cp src/plugins/domtool-postgres /usr/local/sbin/ cp src/plugins/domtool-mysql /usr/local/sbin/ -mkdir -p $(EMACS_DIR) diff --git a/scripts/domtool-vmailpasswd b/scripts/domtool-vmailpasswd new file mode 100755 index 0000000..1ebb365 --- /dev/null +++ b/scripts/domtool-vmailpasswd @@ -0,0 +1,27 @@ +#!/usr/bin/python2 +# -*- python -*- + +# Helper for domtool to check if a vmail password matches the stored +# password, before allowing the portal to change the password. This +# should never be run manually, since it does not suppress echoing of +# anything entered. + +import crypt, getpass, sys + +def getpasswords (): + crypted = raw_input() + clear = raw_input() + return (crypted, clear) + +def checkpassword (crypted, clear): + return crypt.crypt (clear, crypted) == crypted + +def main (): + (crypted, clear) = getpasswords () + if checkpassword (crypted, clear): + sys.exit () + else: + sys.exit (1) + +if __name__ == "__main__": + main () diff --git a/src/mail/vmail.sig b/src/mail/vmail.sig index 4defa43..5289400 100644 --- a/src/mail/vmail.sig +++ b/src/mail/vmail.sig @@ -36,6 +36,9 @@ signature VMAIL = sig val passwd : {domain : string, user : string, passwd : string} -> string option + val portalpasswd : {domain : string, user : string, oldpasswd : string, newpasswd : string} + -> string option + val rm : {domain : string, user : string} -> string option val doChanged : unit -> bool diff --git a/src/mail/vmail.sml b/src/mail/vmail.sml index 8e92f60..0594fb1 100644 --- a/src/mail/vmail.sml +++ b/src/mail/vmail.sml @@ -35,6 +35,43 @@ fun rebuild () = fun doChanged () = Slave.shell [Config.Courier.postReload] + +structure SM = DataStructures.StringMap + +exception Userdb of string + +fun readUserdb domain = + let + val file = OS.Path.joinDirFile {dir = Config.Vmail.userDatabase, + file = domain} + in + if Posix.FileSys.access (file, []) then + let + val inf = TextIO.openIn file + + fun parseField (field, fields) = + case String.fields (fn ch => ch = #"=") field of + [key, value] => SM.insert (fields, key, value) + | _ => raise Userdb ("Malformed fields in vmail userdb for domain " ^ domain) + + fun loop users = + case TextIO.inputLine inf of + NONE => users + | SOME line => + case String.tokens Char.isSpace line of + [addr, fields] => (case String.fields (fn ch => ch = #"@") addr of + [user, _] => + loop (SM.insert (users, user, foldl parseField SM.empty (String.fields (fn ch => ch = #"|") fields))) + | _ => raise Userdb ("Malformed address in vmail userdb for " ^ domain ^ ": " ^ addr)) + | _ => raise Userdb ("Malformed record in vmail userdb for domain " ^ domain) + in + loop SM.empty + before TextIO.closeIn inf + end + else + SM.empty + end + datatype listing = Error of string | Listing of {user : string, mailbox : string} list @@ -112,6 +149,25 @@ fun setpassword {domain, user, passwd} = OS.Process.isSuccess (Unix.reap proc) end + +fun checkpassword {domain, user, passwd} = + let + val proc = Unix.execute (Config.installPrefix ^ "/sbin/domtool-vmailpasswd", []) + val outf = Unix.textOutstreamOf proc + val db = readUserdb domain + in + case SM.find (db, user) of + SOME fields => + (case SM.find (fields, "systempw") of + SOME systempw => + (TextIO.output (outf, systempw ^ "\n"); + TextIO.output (outf, passwd ^ "\n"); + TextIO.closeOut outf; + OS.Process.isSuccess (Unix.reap proc)) + | NONE => raise Userdb ("systempw not found for user " ^ user ^ "@" ^ domain)) + | NONE => raise Userdb ("User " ^ user ^ " not found in vmail userdb for domain " ^ domain) + end + fun deluser {domain, user} = Slave.run (Config.Vmail.userdb, ["-f", Config.Vmail.userDatabase ^ "/" ^ domain, user ^ "@" ^ domain, "del"]) @@ -149,6 +205,19 @@ fun passwd {domain, user, passwd} = else NONE +fun portalpasswd {domain, user, oldpasswd, newpasswd} = + (if not (mailboxExists {domain = domain, user = user}) then + SOME "Mailbox doesn't exist" + else if not (checkpassword {domain = domain, user = user, passwd = oldpasswd}) then + SOME "Old password incorrect" + else if not (setpassword {domain = domain, user = user, passwd = newpasswd}) then + SOME "Error setting password" + else if not (rebuild ()) then + SOME "Error reloading userdb" + else + NONE) + handle Userdb errmsg => SOME ("userdb error: " ^ errmsg) + fun rm {domain, user} = if not (mailboxExists {domain = domain, user = user}) then SOME "Mailbox doesn't exist" diff --git a/src/main-portal.sml b/src/main-portal.sml new file mode 100644 index 0000000..93f7612 --- /dev/null +++ b/src/main-portal.sml @@ -0,0 +1,44 @@ +(* HCoop Domtool (http://hcoop.sourceforge.net/) + * Copyright (c) 2014, Clinton Ebadi + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + *) + +(* Portal helper utility. *) + +(* Duplicated from main-config.sml, should be put into a common module + and all domtool commands should return proper exit codes instead of + always succeeding *) + +fun println x = (print x; print "\n") +fun printerr x = (TextIO.output (TextIO.stdErr, x); TextIO.flushOut TextIO.stdErr) +fun die reason = (printerr reason; printerr "\n"; OS.Process.exit OS.Process.failure) + +val _ = + case CommandLine.arguments () of + ["vmailpasswd", domain, user] => + (case Client.getpass () of + Client.Passwd oldpasswd => + (case Client.getpass () of + Client.Passwd newpasswd => + Main.requestPortalPasswdMailbox {domain = domain, + user = user, + oldpasswd = oldpasswd, + newpasswd = newpasswd} + | Client.Aborted => die "Aborted" + | Client.Error => die "New passwords did not match") + | _ => die "Error entering old password") + | _ => die "Invalid command-line arguments" + diff --git a/src/main.sig b/src/main.sig index 0993234..fa4d398 100644 --- a/src/main.sig +++ b/src/main.sig @@ -68,6 +68,8 @@ signature MAIN = sig passwd : string, mailbox : string} -> unit val requestPasswdMailbox : {domain : string, user : string, passwd : string} -> unit + val requestPortalPasswdMailbox : {domain : string, user : string, oldpasswd : string, newpasswd : string} + -> unit val requestRmMailbox : {domain : string, user : string} -> unit val requestSaQuery : string -> unit diff --git a/src/main.sml b/src/main.sml index 50f5e97..1816f78 100644 --- a/src/main.sml +++ b/src/main.sml @@ -667,6 +667,21 @@ fun requestPasswdMailbox p = OpenSSL.close bio end +fun requestPortalPasswdMailbox p = + let + val (_, bio) = requestBio (fn () => ()) + in + Msg.send (bio, MsgPortalPasswdMailbox p); + case Msg.recv bio of + NONE => print "Server closed connection unexpectedly.\n" + | SOME m => + case m of + MsgOk => print ("The password for " ^ #user p ^ "@" ^ #domain p ^ " has been changed.\n") + | MsgError s => print ("Set failed: " ^ s ^ "\n") + | _ => print "Unexpected server reply.\n"; + OpenSSL.close bio + end + fun requestRmMailbox p = let val (_, bio) = requestBio (fn () => ()) @@ -1520,6 +1535,27 @@ fun service () = SOME msg)) (fn () => ()) + | MsgPortalPasswdMailbox {domain, user = emailUser, oldpasswd, newpasswd} => + doIt (fn () => + if not (Acl.query {user = user, class = "priv", value = "vmail"}) then + ("User is not authorized to run portal vmail password", + SOME "You're not authorized to use the portal password command") + else if not (Domain.validEmailUser emailUser) then + ("Invalid e-mail username " ^ emailUser, + SOME "Invalid e-mail username") + else if not (CharVector.all Char.isGraph oldpasswd + andalso CharVector.all Char.isGraph newpasswd) then + ("Invalid password", + SOME "Invalid password; may only contain printable, non-space characters") + else + case Vmail.portalpasswd {domain = domain, user = emailUser, + oldpasswd = oldpasswd, newpasswd = newpasswd} of + NONE => ("Changed password of mailbox " ^ emailUser ^ "@" ^ domain, + NONE) + | SOME msg => ("Error changing mailbox password for " ^ emailUser ^ "@" ^ domain ^ ": " ^ msg, + SOME msg)) + (fn () => ()) + | MsgRmMailbox {domain, user = emailUser} => doIt (fn () => if not (Domain.yourDomain domain) then diff --git a/src/msg.sml b/src/msg.sml index cbb9059..eb04648 100644 --- a/src/msg.sml +++ b/src/msg.sml @@ -250,6 +250,12 @@ fun send (bio, m) = OpenSSL.writeString (bio, section); OpenSSL.writeString (bio, description)) | MsgSaChanged => OpenSSL.writeInt (bio, 45) + | MsgPortalPasswdMailbox {domain : string, user : string, oldpasswd : string, newpasswd : string} => + (OpenSSL.writeInt (bio, 46); + OpenSSL.writeString (bio, domain); + OpenSSL.writeString (bio, user); + OpenSSL.writeString (bio, oldpasswd); + OpenSSL.writeString (bio, newpasswd)) fun checkIt v = case v of @@ -369,6 +375,10 @@ fun recv bio = (SOME section, SOME description) => SOME (MsgAptQuery {section = section, description = description}) | _ => NONE) | 45 => SOME MsgSaChanged + | 46 => (case (OpenSSL.readString bio, OpenSSL.readString bio, OpenSSL.readString bio, OpenSSL.readString bio) of + (SOME domain, SOME user, SOME oldpasswd, SOME newpasswd) => + SOME (MsgPortalPasswdMailbox {domain = domain, user = user, oldpasswd = oldpasswd, newpasswd = newpasswd}) + | _ => NONE) | _ => NONE) end diff --git a/src/msgTypes.sml b/src/msgTypes.sml index c6dcb50..c815960 100644 --- a/src/msgTypes.sml +++ b/src/msgTypes.sml @@ -88,6 +88,8 @@ datatype msg = (* Request creation of a new vmail mapping *) | MsgPasswdMailbox of {domain : string, user : string, passwd : string} (* Change a vmail account's password *) + | MsgPortalPasswdMailbox of {domain : string, user : string, oldpasswd : string, newpasswd : string} + (* Change a vmail account's password if the old password matches *) | MsgRmMailbox of {domain : string, user : string} (* Remove a vmail mapping *) | MsgListMailboxes of string -- 2.20.1