1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2026-03-05 15:57:23 +01:00

Merge branch 'totp'

This commit is contained in:
downtownallday
2020-10-31 11:39:35 -04:00
47 changed files with 2072 additions and 266 deletions

View File

@@ -1,6 +1,6 @@
# If there aren't any mail users yet, create one.
if [ -z "`tools/mail.py user`" ]; then
# The outut of "tools/mail.py user" is a list of mail users. If there
if [ -z "`management/cli.py user`" ]; then
# The outut of "management/cli.py user" is a list of mail users. If there
# aren't any yet, it'll be empty.
# If we didn't ask for an email address at the start, do so now.
@@ -47,11 +47,11 @@ if [ -z "`tools/mail.py user`" ]; then
fi
# Create the user's mail account. This will ask for a password if none was given above.
tools/mail.py user add $EMAIL_ADDR ${EMAIL_PW:-}
management/cli.py user add $EMAIL_ADDR ${EMAIL_PW:-}
# Make it an admin.
hide_output tools/mail.py user make-admin $EMAIL_ADDR
hide_output management/cli.py user make-admin $EMAIL_ADDR
# Create an alias to which we'll direct all automatically-created administrative aliases.
tools/mail.py alias add administrator@$PRIMARY_HOSTNAME $EMAIL_ADDR > /dev/null
management/cli.py alias add administrator@$PRIMARY_HOSTNAME $EMAIL_ADDR > /dev/null
fi

View File

@@ -374,6 +374,20 @@ add_schemas() {
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f "$ldif" >/dev/null
rm -f "$ldif"
fi
# apply the mfa-totp schema
# this adds the totpUser class to store the totp secret
local schema="mfa-totp.schema"
local cn="mfa-totp"
get_attribute "cn=schema,cn=config" "(&(cn={*}$cn)(objectClass=olcSchemaConfig))" "cn"
if [ -z "$ATTR_DN" ]; then
local ldif="/tmp/$cn.$$.ldif"
schema_to_ldif "$schema" "$ldif" "$cn"
say_verbose "Adding '$cn' schema"
[ $verbose -gt 1 ] && cat "$ldif"
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f "$ldif" >/dev/null
rm -f "$ldif"
fi
}
@@ -560,16 +574,18 @@ apply_access_control() {
# Permission restrictions:
# service accounts (except management):
# can bind but not change passwords, including their own
# can read all attributes of all users but not userPassword
# can read all attributes of all users but not userPassword,
# totpSecret, totpMruToken, totpMruTokenTime, or totpLabel
# can read config subtree (permitted-senders, domains)
# no access to services subtree, except their own dn
# management service account:
# can read and change password and shadowLastChange
# can read and change password, shadowLastChange, and totpSecret
# all other service account permissions are the same
# users:
# can bind and change their own password
# can read and change their own shadowLastChange
# can read attributess of all users except mailaccess
# cannot read or modify totpSecret, totpMruToken, totpMruTokenTime, totpLabel
# can read attributess of other users except mailaccess, totpSecret, totpMruToken, totpMruTokenTime, totpLabel
# no access to config subtree
# no access to services subtree
#
@@ -591,6 +607,10 @@ olcAccess: to attrs=userPassword
by self =wx
by anonymous auth
by * none
olcAccess: to attrs=totpSecret,totpMruToken,totpMruTokenTime,totpLabel
by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
by * none
olcAccess: to attrs=shadowLastChange
by self write
by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write

View File

@@ -17,7 +17,6 @@ source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
source ${STORAGE_ROOT}/ldap/miab_ldap.conf # user-data specific vars
# ### User Authentication
# Have Dovecot query our database, and not system users, for authentication.

View File

@@ -50,6 +50,7 @@ hide_output $venv/bin/pip install --upgrade pip
hide_output $venv/bin/pip install --upgrade \
rtyaml "email_validator>=1.0.0" "exclusiveprocess" \
flask dnspython python-dateutil \
qrcode[pil] pyotp \
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver ldap3
# CONFIGURATION

View File

@@ -183,6 +183,14 @@ def migration_12(env):
conn.close()
def migration_13(env):
# Add the "mfa" table for configuring MFA for login to the control panel.
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
shell("check_call", ["sqlite3", db, "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);"])
###########################################################
def migration_miabldap_1(env):
# This migration step moves users from sqlite3 to openldap
# users table:
@@ -207,7 +215,7 @@ def migration_13(env):
# objectClass: mailGroup
# mail: [source]
# member: [user-dn] # multi-valued
print("Migrating users and aliases from sqlite to ldap")
# Get the ldap server up and running
@@ -241,12 +249,11 @@ def migration_13(env):
ldap.unbind()
conn.close()
def get_current_migration():
ver = 0
while True:
next_ver = (ver + 1)
migration_func = globals().get("migration_%d" % next_ver)
migration_func = globals().get("migration_miabldap_%d" % next_ver)
if not migration_func:
return ver
ver = next_ver
@@ -312,11 +319,67 @@ def run_migrations():
# iterate and try next version...
def run_miabldap_migrations():
if not os.access("/etc/mailinabox.conf", os.W_OK, effective_ids=True):
print("This script must be run as root.", file=sys.stderr)
sys.exit(1)
env = load_environment()
migration_id_file = os.path.join(env['STORAGE_ROOT'], 'mailinabox-ldap.version')
migration_id = 0
if os.path.exists(migration_id_file):
with open(migration_id_file) as f:
migration_id = f.read().strip();
ourver = int(migration_id)
while True:
next_ver = (ourver + 1)
migration_func = globals().get("migration_miabldap_%d" % next_ver)
if not migration_func:
# No more migrations to run.
break
print()
print("Running migration to Mail-in-a-Box LDAP #%d..." % next_ver)
try:
migration_func(env)
except Exception as e:
print()
print("Error running the migration script:")
print()
print(e)
print()
print("Your system may be in an inconsistent state now. We're terribly sorry. A re-install from a backup might be the best way to continue.")
#sys.exit(1)
raise e
ourver = next_ver
# Write out our current version now. Do this sooner rather than later
# in case of any problems.
with open(migration_id_file, "w") as f:
f.write(str(ourver) + "\n")
# iterate and try next version...
if __name__ == "__main__":
if sys.argv[-1] == "--current":
# Return the number of the highest migration.
print(str(get_current_migration()))
elif sys.argv[-1] == "--migrate":
# Perform migrations.
run_migrations()
env = load_environment()
# if miab-ldap already installed, only run miab-ldap migrations
if 'LDAP_USERS_BASE' in env:
run_miabldap_migrations()
# otherwise, run both
else:
run_migrations()
run_miabldap_migrations()

View File

@@ -8,7 +8,7 @@
import uuid, os, sqlite3, ldap3, hashlib
def add_user(env, ldapconn, search_base, users_base, domains_base, email, password, privs, cn=None):
def add_user(env, ldapconn, search_base, users_base, domains_base, email, password, privs, totp, cn=None):
# Add a sqlite user to ldap
# env are the environment variables
# ldapconn is the bound ldap connection
@@ -18,6 +18,7 @@ def add_user(env, ldapconn, search_base, users_base, domains_base, email, passwo
# email is the user's email
# password is the user's current sqlite password hash
# privs is an array of privilege names for the user
# totp contains the list of secrets, mru tokens, and labels
# cn is the user's common name [optional]
#
# the email address should be as-is from sqlite (encoded as
@@ -37,6 +38,7 @@ def add_user(env, ldapconn, search_base, users_base, domains_base, email, passwo
uid = m.hexdigest()
# Attributes to apply to the new ldap entry
objectClasses = [ 'inetOrgPerson','mailUser','shadowAccount' ]
attrs = {
"mail" : email,
"maildrop" : email,
@@ -73,12 +75,19 @@ def add_user(env, ldapconn, search_base, users_base, domains_base, email, passwo
# Choose a surname for the user (required attribute)
attrs["sn"] = cn[cn.find(' ')+1:]
# add TOTP, if enabled
if totp:
objectClasses.append('totpUser')
attrs['totpSecret'] = totp["secret"]
attrs['totpMruToken'] = totp["mru_token"]
attrs['totpMruTokenTime'] = totp["mru_token_time"]
attrs['totpLabel'] = totp["label"]
# Add user
dn = "uid=%s,%s" % (uid, users_base)
print("adding user %s" % email)
ldapconn.add(dn,
[ 'inetOrgPerson','mailUser','shadowAccount' ],
attrs);
ldapconn.add(dn, objectClasses, attrs)
# Create domain entry indicating that we are handling
# mail for that domain
@@ -95,14 +104,37 @@ def add_user(env, ldapconn, search_base, users_base, domains_base, email, passwo
def create_users(env, conn, ldapconn, ldap_base, ldap_users_base, ldap_domains_base):
# iterate through sqlite 'users' table and create each user in
# ldap. returns a map of email->dn
# select users
c = conn.cursor()
c.execute("SELECT email,password,privileges from users")
c.execute("SELECT id, email, password, privileges from users")
users = {}
for row in c:
email=row[0]
password=row[1]
privs=row[2]
dn = add_user(env, ldapconn, ldap_base, ldap_users_base, ldap_domains_base, email, password, privs.split("\n"))
user_id=row[0]
email=row[1]
password=row[2]
privs=row[3]
totp = None
c2 = conn.cursor()
c2.execute("SELECT secret, mru_token, label from mfa where user_id=? and type='totp'", (user_id,));
rowidx = 0
for row2 in c2:
if totp is None:
totp = {
"secret": [],
"mru_token": [],
"mru_token_time": [],
"label": []
}
totp["secret"].append("{%s}%s" % (rowidx, row2[0]))
totp["mru_token"].append("{%s}%s" % (rowidx, row2[1] or ''))
totp["mru_token_time"].append("{%s}%s" % (rowidx, rowidx))
totp["label"].append("{%s}%s" % (rowidx, row2[2] or ''))
rowidx += 1
dn = add_user(env, ldapconn, ldap_base, ldap_users_base, ldap_domains_base, email, password, privs.split("\n"), totp)
users[email] = dn
return users

View File

@@ -329,7 +329,7 @@ rm -f /etc/cron.hourly/mailinabox-owncloud
# and there's a lot they could mess up, so we don't make any users admins of Nextcloud.
# But if we wanted to, we would do this:
# ```
# for user in $(tools/mail.py user admins); do
# for user in $(management/cli.py user admins); do
# sqlite3 $STORAGE_ROOT/owncloud/owncloud.db "INSERT OR IGNORE INTO oc_group_user VALUES ('admin', '$user')"
# done
# ```

View File

@@ -80,9 +80,9 @@ fi
if [ ! -d $STORAGE_ROOT ]; then
mkdir -p $STORAGE_ROOT
fi
if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then
echo $(setup/migrate.py --current) > $STORAGE_ROOT/mailinabox.version
chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox.version
if [ ! -f $STORAGE_ROOT/mailinabox-ldap.version ]; then
echo $(setup/migrate.py --current) > $STORAGE_ROOT/mailinabox-ldap.version
chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox-ldap.version
fi
# Save the global options in /etc/mailinabox.conf so that standalone