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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
# ```
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user