Add vmail command for changing password when you know the current password
authorClinton Ebadi <clinton@unknownlamer.org>
Fri, 9 May 2014 08:40:31 +0000 (04:40 -0400)
committerClinton Ebadi <clinton@unknownlamer.org>
Fri, 9 May 2014 08:40:31 +0000 (04:40 -0400)
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
scripts/domtool-vmailpasswd [new file with mode: 0755]
src/mail/vmail.sig
src/mail/vmail.sml
src/main-portal.sml [new file with mode: 0644]
src/main.sig
src/main.sml
src/msg.sml
src/msgTypes.sml

index 7b85747..89ae22c 100644 (file)
--- 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 \
 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
 
 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
 
        $(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*" \
 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-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 >$@
 
 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-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/
        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-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)
        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 (executable)
index 0000000..1ebb365
--- /dev/null
@@ -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 ()
index 4defa43..5289400 100644 (file)
@@ -36,6 +36,9 @@ signature VMAIL = sig
     val passwd : {domain : string, user : string, passwd : string}
                 -> string option
 
     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
     val rm : {domain : string, user : string} -> string option
 
     val doChanged : unit -> bool
index 8e92f60..0594fb1 100644 (file)
@@ -35,6 +35,43 @@ fun rebuild () =
 fun doChanged () =
     Slave.shell [Config.Courier.postReload]
 
 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
 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
 
        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"])
 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
 
     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"
 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 (file)
index 0000000..93f7612
--- /dev/null
@@ -0,0 +1,44 @@
+(* HCoop Domtool (http://hcoop.sourceforge.net/)
+ * Copyright (c) 2014, Clinton Ebadi <clinton@unknownlamer.org>
+ *
+ * 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"
+       
index 0993234..fa4d398 100644 (file)
@@ -68,6 +68,8 @@ signature MAIN = sig
                             passwd : string, mailbox : string} -> unit
     val requestPasswdMailbox : {domain : string, user : string, passwd : string}
                               -> unit
                             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
     val requestRmMailbox : {domain : string, user : string} -> unit
 
     val requestSaQuery : string -> unit
index 50f5e97..1816f78 100644 (file)
@@ -667,6 +667,21 @@ fun requestPasswdMailbox p =
        OpenSSL.close bio
     end
 
        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 () => ())
 fun requestRmMailbox p =
     let
        val (_, bio) = requestBio (fn () => ())
@@ -1520,6 +1535,27 @@ fun service () =
                                                               SOME msg))
                                      (fn () => ())
 
                                                               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
                               | MsgRmMailbox {domain, user = emailUser} =>
                                 doIt (fn () =>
                                          if not (Domain.yourDomain domain) then
index cbb9059..eb04648 100644 (file)
@@ -250,6 +250,12 @@ fun send (bio, m) =
                                               OpenSSL.writeString (bio, section);
                                               OpenSSL.writeString (bio, description))
       | MsgSaChanged => OpenSSL.writeInt (bio, 45)
                                               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
 
 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
                                (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
                   | _ => NONE)
         
 end
index c6dcb50..c815960 100644 (file)
@@ -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 *)
        (* 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
        | MsgRmMailbox of {domain : string, user : string}
        (* Remove a vmail mapping *)
        | MsgListMailboxes of string