1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-04-05 00:27:25 +00:00

Merge branch 'master' into EHDD

This commit is contained in:
downtownallday 2020-06-21 17:21:17 -04:00
commit 7137fb6556
56 changed files with 3151 additions and 277 deletions

View File

@ -1,61 +1,53 @@
# travisci config
env:
global:
- NONINTERACTIVE=1
- SKIP_NETWORK_CHECKS=1
- PRIMARY_HOSTNAME=box.abc.com
- MIAB_LDAP_PROJECT=true
language: shell
os: linux
dist: bionic
jobs:
fast_finish: true
include:
# JOB: MiaB-LDAP connected to a remote Nextcloud
- env:
- PRIMARY_HOSTNAME=box1.abc.com
- FEATURE_MUNIN=false
name: remote-nextcloud-docker
before_install:
- echo "==== DUMP ENVIRONMENT ===="
- echo "==== ENVIRONMENT ===="
- env | sort
- echo "UMASK=$(umask)"
#
- echo "==== DUMP AppArmor Status ===="
- echo "==== AppArmor Status ===="
- (sudo aa-status; true)
#
- echo "==== System update ===="
# Do not run 'upgrade' - takes too long
- sudo apt-get update
#
- echo "==== Install QA/test prerequisites ===="
# python3-dnspython is used by the python scripts in 'tests' and is
# not installed by setup
- sudo apt-get -y install python3-dnspython
# avoid the lengthy generation of DH params by copying in a prebuilt file
- sudo mkdir -p /home/user-data/ssl
- sudo cp ./tests/assets/ssl/dh2048.pem /home/user-data/ssl
#
- echo "==== Add the PRIMARY_HOSTNAME to /etc/hosts ===="
# The PRIMARY_HOSTNAME should point to the interface address not
# loopback. That is because of the way MiaB resolves - the local
# resolver is bind9, which requires valid NS records, which would
# point back to the local nsd authoritative name server for the
# domain. We don't have those in QA, so we need the hosts entry.
- echo "$(source setup/functions.sh; get_default_privateip 4) $PRIMARY_HOSTNAME" > /tmp/hosts_add.tmp
- sudo $SHELL -c 'cat /tmp/hosts_add.tmp >>/etc/hosts'
- echo "==== NETWORK INFO ===="
- hostname -I
- hostname -i
- hostname
- hostname --fqdn
- ip add
- sysctl -a 2>/dev/null | grep -i ipv6 | grep disable
install:
- sudo ./setup/start.sh -v
# setup with 'basic' data before setting up again using
# a remote nextcloud to verify ownCloud contacts are still
# available
- sudo tests/system-setup/remote-nextcloud-docker.sh upgrade basic
script:
# nsd won't start on Travis without the changes below: ip6 off and
# control-enable set to no. Even though the nsd docs says the
# default value for control-enable is no, running "nsd-checkconf -o
# control-enable /etc/nsd/nsd.conf" returns "yes", so we explicitly
# set it here.
#
# we're assuming that the "ip-address" line is the last line in the
# "server" section of nsd.conf. if this generated file output
# changes, the sed command below may need to be adjusted.
- sudo sed -i 's/ip-address\(.\)\(.*\)/ip-address\1\2\n do-ip4\1 yes\n do-ip6\1 no\n verbosity\1 3\nremote-control\1\n control-enable\1 no/' /etc/nsd/nsd.conf
- sudo cat /etc/nsd/nsd.conf
- sudo systemctl reset-failed nsd.service
- sudo systemctl restart nsd.service
#
# launch automated tests, but skip tests that require remote
# smtp support because Travis-CI blocks outgoing port 25
- sudo ./tests/runner.sh -dumpoutput -no-smtp-remote
- sudo touch /etc/dovecot/sieve-spam.svbin
- sudo tests/runner.sh -dumpoutput -no-smtp-remote default remote-nextcloud upgrade-basic
# JOB: Upgrade from upstream install
- env:
- PRIMARY_HOSTNAME=box2.abc.com
- UPSTREAM_TAG=master
name: upgrade-from-upstream
install:
- sudo tests/system-setup/upgrade-from-upstream.sh basic
script:
# launch automated tests, but skip tests that require remote
# smtp support because Travis-CI blocks outgoing port 25
- sudo touch /etc/dovecot/sieve-spam.svbin
- sudo tests/runner.sh -dumpoutput -no-smtp-remote default upgrade-basic

View File

@ -9,6 +9,17 @@ Mail:
* An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed.
* MTA-STS reporting is enabled with reports sent to administrator@ the primary hostname.
DNS:
* autoconfig and autodiscover subdomains and CalDAV/CardDAV SRV records are no longer generated for domains that don't have user accounts since they are unnecessary.
v0.46 (June 11, 2020)
---------------------
Security fixes:
* Roundcube is updated to version 1.4.6 (https://roundcube.net/news/2020/06/02/security-updates-1.4.5-and-1.3.12).
v0.45 (May 16, 2020)
--------------------

View File

@ -1,34 +1,49 @@
[![Build Status](https://travis-ci.com/downtownallday/mailinabox-ldap.svg?branch=master)](https://travis-ci.com/downtownallday/mailinabox-ldap)
Mail-in-a-Box LDAP
===================
# Mail-in-a-Box LDAP
This is a version of [Mail-in-a-Box](https://mailinabox.email) with LDAP used as the user account database instead of sqlite.
All features are supported - you won't find many visible differences. It's only an under-the-hood change.
However it will allow a remote Nextcloud installation to authenticate users against Mail-in-a-Box using [Nextcloud's official LDAP support](https://nextcloud.com/usermanagement/). A single user account database shared with Nextcloud was originally the goal of the project which would simplify deploying a private mail and cloud service for a home or small business. But, there could be many other use cases as well.
However, it will allow a remote Nextcloud installation to authenticate users against Mail-in-a-Box using [Nextcloud's official LDAP support](https://nextcloud.com/usermanagement/). A single user account database shared with Nextcloud was originally the goal of the project which would simplify deploying a private mail and cloud service for a home or small business. But, there could be many other use cases as well.
To add a new account to Nextcloud, you'd simply add a new email account with MiaB-LDAP's admin interface. Quotas and other account settings are made within Nextcloud.
How to connect a remote Nextcloud
## How to connect to a remote Nextcloud
---------------------------------
To fully integrate Mail-in-a-Box w/LDAP (MiaB-LDAP) with Nextcloud, changes must be made on both sides.
To integrate Mail-in-a-Box w/LDAP (MiaB-LDAP) with Nextcloud, changes must be made on both sides. These changes are automated.
1. MiaB-LDAP
* Remote LDAPS access: the default MiaB-LDAP installation doesn't allow any remote LDAP access, so for Nextcloud to access MiaB-LDAP, firewall rules must be loosened to the LDAPS port (636). This is a one-time change. Run something like this as root on MiaB-LDAP, where $ip is the ip-address of your Nextcloud server: `ufw allow proto tcp from $ip to any port ldaps`
* Roundcube and Z-Push (ActiveSync) changes: modify the MiaB-LDAP configuration to use the remote Nextcloud for contacts and calendar. A script to do this automatically will be available soon.
2. Remote Nextcloud
* Use MiaB-LDAP for user acccounts: on Nextcloud, enable user-ldap (in Apps, enable "LDAP user and group backend". Then in Settings click on "LDAP / AD integration". There are quite a few settings to make in there and more information on this will be forthcoming, including a script that will use the user-ldap API to configure the LDAP parameters in Nextcloud for you.
**On MiaB-LDAP**
Details
-------
Enable the setup mod `remote-nextcloud.sh` by creating the directory `local` in the directory where mailinabox is installed (usually $HOME/mailinabox), then creat a symbolic link to remote-nextcloud.sh. e.g. run this command from the mailinabox directory: `mkdir -p local; ln -s ../setup/mods.available/remote-nextcloud.sh local/remote-nextcloud.sh`. *During setup you will be prompted for the hostname and web prefix of your remote Nextcloud box.*
Once installed, you will find all LDAP service account credentials in `/home/user-data/ldap/miab_ldap.conf`, such as those for Nextcloud. Service accounts have limited rights to make changes and should be preferred over the use of the LDAP admin account.
The setup mod will configure Roundcube and Z-Push (ActiveSync) to use the remote Nextcloud for contacts and calendar instead of the local Nextcloud, which will be disabled (browsing to /cloud will fail). Old contacts will still be available in Roundcube, but read-only. Users can drag them into the remote Nextcloud.
**On the remote Nextcloud**
Copy the file `setup/mods.available/remote-nextcloud-use-miab.sh` to the Nextcloud box and run it. This will configure Nextcloud's "LDAP user and group backend" with the MiaB-LDAP details and ensure the contacts and calendar apps are installed. *This does not replace or alter your ability to log into Nextcloud with any existing local Nextcloud accounts. It only allows MiaB-LDAP users to log into Nextcloud using their MiaB-LDAP credentials.*
**Additional Firewall Rule**
On MiaB-LDAP, a one-time change must be applied manually to allow the remote Nextcloud to query the LDAP server because the default MiaB-LDAP installation doesn't allow any remote LDAP access. As root, run the following: `ufw allow proto tcp from $ip to any port ldaps`, where $ip is the ip-address of your Nextcloud server.
## Under-the-Hood
**Additional directory in user-data**
A new ldap directory is created by setup under STORAGE_ROOT (/home/user-data/ldap) that holds the LDAP database, so that it gets backed up by the normal backup process. In there, you will also find all LDAP service account credentials created by setup in `/home/user-data/ldap/miab_ldap.conf`, such as those for Nextcloud. Service accounts have limited rights to make changes and should be preferred over the use of the LDAP admin account.
**LDAP schema for postfix and dovecot**
See `conf/postfix.schema` for more details on the LDAP schema.
LDAP server access logs are stored in `/var/log/ldap/slapd.log` and rotated daily.
**LDAP logs**
LDAP server logs are stored in `/var/log/ldap/slapd.log` and rotated daily.
**Command line queries**
To perform general command-line searches against your LDAP database, run `setup/ldap -search "\<query\>"` as root, where _query_ can be a distinguished name to show all attributes of that dn, or an LDAP search enclosed in parenthesis. Some examples:
* `setup/ldap.sh -search "(mail=alice@mydomain.com)"` (show alice)
@ -36,12 +51,14 @@ To perform general command-line searches against your LDAP database, run `setup/
* `setup/ldap.sh -search "(objectClass=mailuser)"` (show all users)
* etc.
This is a convenient way to run ldapsearch to with all the correct command line arguments.
This is a convenient way to run ldapsearch having all the correct command line arguments, but any LDAP tool will also work.
Caution: do not make LDAP database changes, such as adding users or groups directly using ldapmodify or any other LDAP database tools. Use the MiaB admin interface or REST API! Adding or removing a user or group with the admin interface may trigger additional database and system changes by the management daemon, such as updating DNS zones for new email domains, updating group memberships, etc.
**Caution**
*Do not make direct LDAP database changes, such as adding users or groups using ldapmodify or other LDAP database tools. Instead, use the MiaB admin interface or REST API. Adding or removing a user or group with the admin interface may trigger additional database and system changes by the management daemon, such as updating DNS zones for new email domains, updating group memberships, etc, that would not be performed with a direct change.*
Migration
## Migration
---------
When installing MiaB-LDAP by running any of the setup scripts (`miab`, `setup/bootstrap.sh`, `setup/start.sh`, etc) will automatically migrate your current installation to LDAP. Make a backup before running!
Running any of the setup scripts to install MiaB-LDAP (`miab`, `setup/bootstrap.sh`, `setup/start.sh`, etc) will automatically migrate your current installation from sqlite to LDAP. Make a full MiaB backup before running!

View File

@ -203,10 +203,11 @@ class LdapConnection(ldap3.Connection):
existing_record,
values[attr])
if modify_op: changes[attr] = modify_op
if len(changes)>0:
self.wait ( self.modify(dn, changes) )
return 'modify'
else:
# add new alias
# add new entry
self.wait ( self.add(dn, objectClasses, values) )
return 'add'

View File

@ -16,10 +16,10 @@ if [ `date "+%u"` -eq 1 ]; then
fi
# Take a backup.
management/backup.py | management/email_administrator.py "Backup Status"
management/backup.py 2>&1 | management/email_administrator.py "Backup Status"
# Provision any new certificates for new domains or domains with expiring certificates.
management/ssl_certificates.py -q | management/email_administrator.py "TLS Certificate Provisioning Result"
management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result"
# Run status checks and email the administrator if anything changed.
management/status_checks.py --show-changes | management/email_administrator.py "Status Checks Change Notice"
management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice"

View File

@ -282,18 +282,20 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en
if not has_rec(dmarc_qname, "TXT", prefix="v=DMARC1; "):
records.append((dmarc_qname, "TXT", 'v=DMARC1; p=reject', "Recommended. Prevents use of this domain name for outbound mail by specifying that the SPF rule should be honoured for mail from @%s." % (qname + "." + domain)))
# Add CardDAV/CalDAV SRV records on the non-primary hostname that points to the primary hostname.
# Add CardDAV/CalDAV SRV records on the non-primary hostname that points to the primary hostname
# for autoconfiguration of mail clients (so only domains hosting user accounts need it).
# The SRV record format is priority (0, whatever), weight (0, whatever), port, service provider hostname (w/ trailing dot).
if domain != env["PRIMARY_HOSTNAME"]:
if domain != env["PRIMARY_HOSTNAME"] and domain in get_mail_domains(env, users_only=True):
for dav in ("card", "cal"):
qname = "_" + dav + "davs._tcp"
if not has_rec(qname, "SRV"):
records.append((qname, "SRV", "0 0 443 " + env["PRIMARY_HOSTNAME"] + ".", "Recommended. Specifies the hostname of the server that handles CardDAV/CalDAV services for email addresses on this domain."))
# Adds autoconfiguration A records for all domains.
# Adds autoconfiguration A records for all domains that there are user accounts at.
# This allows the following clients to automatically configure email addresses in the respective applications.
# autodiscover.* - Z-Push ActiveSync Autodiscover
# autoconfig.* - Thunderbird Autoconfig
if domain in get_mail_domains(env, users_only=True):
autodiscover_records = [
("autodiscover", "A", env["PUBLIC_IP"], "Provides email configuration autodiscovery support for Z-Push ActiveSync Autodiscover."),
("autodiscover", "AAAA", env["PUBLIC_IPV6"], "Provides email configuration autodiscovery support for Z-Push ActiveSync Autodiscover."),
@ -339,11 +341,13 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en
# 'break' was not encountered above, so both domains are good
mta_sts_enabled = True
if mta_sts_enabled:
# Compute a up-to-32-character hash of the policy file. We'll take a SHA-1 hash of the policy
# file (20 bytes) and encode it as base-64 (60 bytes) but then just take its first 20 bytes
# which should be sufficient to change whenever the policy file changes.
# Compute an up-to-32-character hash of the policy file. We'll take a SHA-1 hash of the policy
# file (20 bytes) and encode it as base-64 (28 bytes, using alphanumeric alternate characters
# instead of '+' and '/' which are not allowed in an MTA-STS policy id) but then just take its
# first 20 characters, which is more than sufficient to change whenever the policy file changes
# (and ensures any '=' padding at the end of the base64 encoding is dropped).
with open("/var/lib/mailinabox/mta-sts.txt", "rb") as f:
mta_sts_policy_id = base64.b64encode(hashlib.sha1(f.read()).digest()).decode("ascii")[0:20]
mta_sts_policy_id = base64.b64encode(hashlib.sha1(f.read()).digest(), altchars=b"AA").decode("ascii")[0:20]
mta_sts_records.extend([
("_mta-sts", "TXT", "v=STSv1; id=" + mta_sts_policy_id, "Optional. Part of the MTA-STS policy for incoming mail. If set, a MTA-STS policy must also be published.")
])

View File

@ -10,7 +10,7 @@
# Python 3 in setup/questions.sh to validate the email
# address entered by the user.
import subprocess, shutil, os, sqlite3, re, ldap3, uuid
import subprocess, shutil, os, sqlite3, re, ldap3, uuid, hashlib
import utils, backend
from email_validator import validate_email as validate_email_, EmailNotValidError
import idna
@ -362,7 +362,7 @@ def get_mail_aliases(env, as_map=False):
alias = aliases[address]
xft = ",".join(alias["forward_tos"])
xas = ",".join(alias["permitted_senders"])
list.append( (address, xft, xas) )
list.append( (address, xft, None if xas == "" else xas) )
return list
else:
@ -432,7 +432,7 @@ def get_domain(emailaddr, as_unicode=True):
pass
return ret
def get_mail_domains(env, as_map=False, filter_aliases=None, category=None):
def get_mail_domains(env, as_map=False, filter_aliases=lambda alias: True, category=None, users_only=False):
# Retrieves all domains, IDNA-encoded, we accept mail for.
#
# If as_map is False, the function returns the lowercase domain
@ -453,16 +453,22 @@ def get_mail_domains(env, as_map=False, filter_aliases=None, category=None):
# category is another type of filter. Set to a string value to
# return only those domains of that category. ie. the
# "businessCategory" attribute of the domain must include this
# category.
# category. [TODO: this doesn't really belong there, it is here to
# make it easy for dns_update to get ssl domains]
#
# If users_only is True, only return domains with email addresses
# that correspond to user accounts.
#
conn = open_database(env)
filter = "(&(objectClass=domain)(businessCategory=mail))"
if category:
filter = "(&(objectClass=domain)(businessCategory=%s))" % category
domains=None
# user mail domains
id = conn.search(env.LDAP_DOMAINS_BASE, filter, attributes="dc")
response = conn.wait(id)
filter_candidates=[]
domains=None
if as_map:
domains = {}
for rec in response:
@ -473,35 +479,43 @@ def get_mail_domains(env, as_map=False, filter_aliases=None, category=None):
if filter_aliases: filter_candidates.append(rec['dc'][0].lower())
else:
domains = set([ rec["dc"][0].lower() for rec in response ])
if filter_aliases: filter_candidates += domains
for candidate in filter_candidates:
# with the filter, there has to be at least one user or
# filtered (included) alias in the domain for the domain to be
# part of the returned set
# any users ?
response = conn.wait( conn.search(env.LDAP_USERS_BASE, "(&(objectClass=mailUser)(mail=*@%s))" % candidate, size_limit=1) )
if response.next():
# yes, that domain needs to be in the returned set
continue
# any filtered aliases ?
pager = conn.paged_search(
env.LDAP_ALIASES_BASE,
"(&(objectClass=mailGroup)(mail=*@%s))" % candidate,
attributes=['mail'])
remove = True
# alias domains
#
# Ignore aliases that have no forward-to. We don't need DNS
# handling in that case becuase the alias is there only for the
# permitted-senders. We don't accept mail locally for the alias.
#
# Aliases with only permitted-senders are useful when a server has
# a configured smarthost (eg. sendmail with a smarthost, or using
# ssmtp on Ubuntu, etc). The server drops mail off for delivery to
# the smarthost (MiaB) using its MiaB login but needs to MAIL FROM
# a host login (user@host.tld). Replies should bounce.
#
# A smarthost configuration should be a catch-all, one for each server:
# Alias=@host.tld
# Forward-to=<blank>
# Permitted-senders:<the email that the smarthost used to authenticate with MiaB>
#
if not users_only:
pager = conn.paged_search(env.LDAP_ALIASES_BASE, "(objectClass=mailGroup)", attributes=["mail","member","rfc822MailMember"])
if as_map:
for rec in pager:
if filter_aliases(rec['mail'][0]):
remove = False
pager.abandon()
break
if filter_aliases(rec["mail"][0].lower()) and ( len(rec["member"]) >0 or len(rec["rfc822MailMember"]) >0 ):
domain = get_domain(rec["mail"][0].lower(),as_unicode=False)
domains[domain] = {
"dn": None,
"domain": domain
}
if remove:
if as_map: del domains[candidate]
else: domains.remove(candidate)
else:
alias_domains = set([
get_domain(rec["mail"][0].lower(), as_unicode=False)
for rec in pager if filter_aliases(rec["mail"][0].lower()) and
( len(rec["member"]) >0 or len(rec["rfc822MailMember"]) >0 )
])
domains = domains.union( alias_domains )
return domains
@ -637,8 +651,12 @@ def add_mail_user(email, pw, privs, env):
if conn.wait(id).count() > 0:
return ("An alias exists with that address.", 400)
# Generate a unique id for uid
uid = '%s' % uuid.uuid4()
## Generate a unique id for uid
#uid = '%s' % uuid.uuid4()
# use a sha-1 hash of maildrop for uid
m = hashlib.sha1()
m.update(bytearray(email.lower(),'utf-8'))
uid = m.hexdigest()
# choose a common name and surname (required attributes)
cn = email.split("@")[0].replace('.',' ').replace('_',' ')

View File

@ -26,14 +26,13 @@ def get_web_domains(env, include_www_redirects=True, exclude_dns_elsewhere=True,
# the topmost of each domain we serve.
domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env))
# Add Autoconfiguration domains, allowing us to serve correct SSL certs.
# Add Autoconfiguration domains for domains that there are user accounts at:
# 'autoconfig.' for Mozilla Thunderbird auto setup.
# 'autodiscover.' for Activesync autodiscovery.
if 'mail' in categories:
domains |= set('autoconfig.' + maildomain for maildomain in get_mail_domains(env, category='mail'))
domains |= set('autodiscover.' + maildomain for maildomain in get_mail_domains(env, category='mail'))
# 'mta-sts.' for MTA-STS support.
domains |= set('autoconfig.' + maildomain for maildomain in get_mail_domains(env, users_only=True))
domains |= set('autodiscover.' + maildomain for maildomain in get_mail_domains(env, users_only=True))
# 'mta-sts.' for MTA-STS support for all domains that have email addresses.
domains |= set('mta-sts.' + maildomain for maildomain in get_mail_domains(env))
if exclude_dns_elsewhere:
@ -161,9 +160,23 @@ def make_domain_config(domain, templates, ssl_certificates, env):
# any proxy or redirect here?
for path, url in yaml.get("proxies", {}).items():
# Parse some flags in the fragment of the URL.
pass_http_host_header = False
m = re.search("#(.*)$", url)
if m:
for flag in m.group(1).split(","):
if flag == "pass-http-host":
pass_http_host_header = True
url = re.sub("#(.*)$", "", url)
nginx_conf_extra += "\tlocation %s {" % path
nginx_conf_extra += "\n\t\tproxy_pass %s;" % url
if pass_http_host_header:
nginx_conf_extra += "\n\t\tproxy_set_header Host $http_host;"
nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;"
nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-Host $http_host;"
nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-Proto $scheme;"
nginx_conf_extra += "\n\t\tproxy_set_header X-Real-IP $remote_addr;"
nginx_conf_extra += "\n\t}\n"
for path, alias in yaml.get("aliases", {}).items():
nginx_conf_extra += "\tlocation %s {" % path

View File

@ -20,7 +20,7 @@ if [ -z "$TAG" ]; then
# want to display in status checks.
if [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' `" == "Ubuntu 18.04 LTS" ]; then
# This machine is running Ubuntu 18.04.
TAG=v0.45
TAG=v0.46
elif [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' `" == "Ubuntu 14.04 LTS" ]; then
# This machine is running Ubuntu 14.04.

View File

@ -62,6 +62,24 @@ for ip in $PRIVATE_IP $PRIVATE_IPV6; do
echo " ip-address: $ip" >> /etc/nsd/nsd.conf;
done
# nsd fails to start when ipv6 is disabled by the kernel on certain
# interfaces without "do-ip6" set to "no" and "control-enable" to "no"
# [confirm]. Even though the nsd docs say the default value for
# control-enable is no, running "nsd-checkconf -o control-enable
# /etc/nsd/nsd.conf" returns "yes", so we explicitly set it here.
#
# For instance, on Travis-CI, ipv6 is disabled on the lo and docker
# interfaces, but enabled on the primary interface ens4. nsd fails to
# start without these additions.
if kernel_ipv6_lo_disabled; then
cat >> /etc/nsd/nsd.conf <<EOF
do-ip4: yes
do-ip6: no
remote-control:
control-enable: no
EOF
fi
echo "include: /etc/nsd/zones.conf" >> /etc/nsd/nsd.conf;
# Create DNSSEC signing keys.

View File

@ -137,7 +137,14 @@ function get_default_privateip {
function ufw_allow {
if [ -z "${DISABLE_FIREWALL:-}" ]; then
# ufw has completely unhelpful output
ufw allow $1 > /dev/null;
ufw allow "$1" > /dev/null;
fi
}
function ufw_limit {
if [ -z "${DISABLE_FIREWALL:-}" ]; then
# ufw has completely unhelpful output
ufw limit "$1" > /dev/null;
fi
}
@ -235,3 +242,9 @@ function generate_password() {
dd if=/dev/urandom bs=1 count=$input_len 2>/dev/null | base64 --wrap=0 | awk '{ gsub("/", ",", $0); print $0}'
}
function kernel_ipv6_lo_disabled() {
# Returns 0 if ipv6 is disabled on the loopback adapter
local v="$(sysctl -n net.ipv6.conf.lo.disable_ipv6)"
[ "$v" == "1" ] && return 0
return 0
}

View File

@ -71,42 +71,46 @@ wait_slapd_start() {
say_verbose "...ok"
}
_add_if_missing() {
local var="$1"
local val="$2"
local conf="$MIAB_INTERNAL_CONF_FILE"
if [ $(grep -c "^${var}=" "$conf") -eq 0 ]; then
echo "${var}=\"${val}\"" >> "$conf"
fi
}
create_miab_conf() {
# create (if non-existing) or load (existing) ldap/miab_ldap.conf
if [ ! -e "$MIAB_INTERNAL_CONF_FILE" ]; then
say_verbose "Generating a new $MIAB_INTERNAL_CONF_FILE"
mkdir -p "$(dirname $MIAB_INTERNAL_CONF_FILE)"
# Use 64-character secret keys of safe characters
cat > "$MIAB_INTERNAL_CONF_FILE" <<EOF
LDAP_SERVER=127.0.0.1
LDAP_SERVER_PORT=389
LDAP_SERVER_STARTTLS=no
LDAP_SERVER_TLS=no
LDAP_URL=ldap://127.0.0.1/
LDAP_BASE="${LDAP_BASE}"
LDAP_SERVICES_BASE="${LDAP_SERVICES_BASE}"
LDAP_CONFIG_BASE="${LDAP_CONFIG_BASE}"
LDAP_DOMAINS_BASE="${LDAP_DOMAINS_BASE}"
LDAP_PERMITTED_SENDERS_BASE="${LDAP_PERMITTED_SENDERS_BASE}"
LDAP_USERS_BASE="${LDAP_USERS_BASE}"
LDAP_ALIASES_BASE="${LDAP_ALIASES_BASE}"
LDAP_ADMIN_DN="${LDAP_ADMIN_DN}"
LDAP_ADMIN_PASSWORD="$(generate_password 64)"
EOF
touch "$MIAB_INTERNAL_CONF_FILE"
fi
# ensure all required values exist, and if not set to default values
_add_if_missing LDAP_SERVER 127.0.0.1
_add_if_missing LDAP_SERVER_PORT 389
_add_if_missing LDAP_SERVER_STARTTLS no
_add_if_missing LDAP_SERVER_TLS no
_add_if_missing LDAP_URL ldap://127.0.0.1/
_add_if_missing LDAP_BASE "${LDAP_BASE}"
_add_if_missing LDAP_SERVICES_BASE "${LDAP_SERVICES_BASE}"
_add_if_missing LDAP_CONFIG_BASE "${LDAP_CONFIG_BASE}"
_add_if_missing LDAP_DOMAINS_BASE "${LDAP_DOMAINS_BASE}"
_add_if_missing LDAP_PERMITTED_SENDERS_BASE "${LDAP_PERMITTED_SENDERS_BASE}"
_add_if_missing LDAP_USERS_BASE "${LDAP_USERS_BASE}"
_add_if_missing LDAP_ALIASES_BASE "${LDAP_ALIASES_BASE}"
_add_if_missing LDAP_ADMIN_DN "${LDAP_ADMIN_DN}"
_add_if_missing LDAP_ADMIN_PASSWORD "$(generate_password 64)"
# add service account credentials
local prefix
for prefix in ${SERVICE_ACCOUNTS[*]}
do
if [ $(grep -c "^$prefix" "$MIAB_INTERNAL_CONF_FILE") -eq 0 ]; then
local cn=$(awk -F_ '{print tolower($2)}' <<< $prefix)
cat >>"$MIAB_INTERNAL_CONF_FILE" <<EOF
${prefix}_DN="cn=$cn,$LDAP_SERVICES_BASE"
${prefix}_PASSWORD="$(generate_password 64)"
EOF
fi
_add_if_missing "${prefix}_DN" "cn=$cn,$LDAP_SERVICES_BASE"
_add_if_missing "${prefix}_PASSWORD" "$(generate_password 64)"
done
chmod 0640 "$MIAB_INTERNAL_CONF_FILE"
@ -853,7 +857,13 @@ cat > /etc/logrotate.d/slapd <<EOF;
EOF
# Modify olc server config like TLS
# Skip this step if no ca_certificate.pem exists - this indicates
# that the system hasn't yet been migrated from sqlite
if [ -e "$STORAGE_ROOT/ssl/ca_certificate.pem" ]; then
modify_global_config
else
say_debug "Not enabling TLS at this time - ca_certificate hasn't been generated yet"
fi
# Add overlays and ensure mail-related attributes are indexed
add_overlays

View File

@ -5,7 +5,7 @@
# helper functions for migration #13
#
import uuid, os, sqlite3, ldap3
import uuid, os, sqlite3, ldap3, hashlib
def add_user(env, ldapconn, search_base, users_base, domains_base, email, password, privs, cn=None):
@ -29,8 +29,12 @@ def add_user(env, ldapconn, search_base, users_base, domains_base, email, passwo
print("user already exists: %s" % email)
return ldapconn.response[0]['dn']
# Generate a unique id for uid
uid = '%s' % uuid.uuid4()
## Generate a unique id for uid
#uid = '%s' % uuid.uuid4()
# use a sha-1 hash of the email address for uid
m = hashlib.sha1()
m.update(bytearray(email.lower(),'utf-8'))
uid = m.hexdigest()
# Attributes to apply to the new ldap entry
attrs = {

View File

@ -0,0 +1,16 @@
<?php
/***********************************************
* File : config.php
* Project : Z-Push
* Descr : CalDAV backend configuration file
************************************************/
define('CALDAV_PROTOCOL', 'NC_PROTO');
define('CALDAV_SERVER', 'NC_HOST');
define('CALDAV_PORT', 'NC_PORT');
define('CALDAV_PATH', 'NC_PREFIX/remote.php/dav/calendars/%u/');
define('CALDAV_PERSONAL', 'PRINCIPAL');
define('CALDAV_SUPPORTS_SYNC', false);
define('CALDAV_MAX_SYNC_PERIOD', 2147483647);
?>

View File

@ -0,0 +1,31 @@
<?php
/***********************************************
* File : config.php
* Project : Z-Push
* Descr : CardDAV backend configuration file
************************************************/
define('CARDDAV_PROTOCOL', 'NC_PROTO'); /* http or https */
define('CARDDAV_SERVER', 'NC_HOST');
define('CARDDAV_PORT', 'NC_PORT');
define('CARDDAV_PATH', 'NC_PREFIX/remote.php/dav/addressbooks/users/%u/');
define('CARDDAV_DEFAULT_PATH', 'NC_PREFIX/remote.php/dav/addressbooks/users/%u/contacts/'); /* subdirectory of the main path */
define('CARDDAV_GAL_PATH', ''); /* readonly, searchable, not syncd */
define('CARDDAV_GAL_MIN_LENGTH', 5);
define('CARDDAV_CONTACTS_FOLDER_NAME', '%u Addressbook');
define('CARDDAV_SUPPORTS_SYNC', false);
// If the CardDAV server supports the FN attribute for searches
// DAViCal supports it, but SabreDav, Nextcloud and SOGo don't
// Setting this to true will search by FN. If false will search by sn, givenName and email
// It's safe to leave it as false
define('CARDDAV_SUPPORTS_FN_SEARCH', false);
// If your carddav server needs to use file extension to recover a vcard.
// Davical needs it
// SOGo official demo online needs it, but some SOGo installation don't need it, so test it
define('CARDDAV_URL_VCARD_EXTENSION', '.vcf');
?>

View File

@ -0,0 +1,398 @@
#!/bin/bash
#
# Run this script on your remote Nextcloud to configure it to use
# Mail-in-a-Box-LDAP.
#
# The script will:
# 1. enable the "LDAP user and group backend" in Nextcloud
# 2. install calendar and contacts
# 3. configure Nextcloud to access MiaB-LDAP for users and groups
# 4. optionally install and configure ssmtp so system mail is
# sent to MiaB-LDAP
#
VERBOSE=0
usage() {
cat <<EOF
Usage: $0 <NCDIR> <NC_ADMIN_USER> <NC_ADMIN_PASSWORD> <MIAB_HOSTNAME> <LDAP_NEXTCLOUD_PASS> [ <SSMTP_ALERTS_EMAIL> <SSMTP_AUTH_USER> <SSMTP_AUTH_PASS> ]
Configure Nextcloud to use MiaB-LDAP for users and groups
Optionally configure a mail relay to MiaB-LDAP
Arguments:
NCDIR
the path to the local Nextcloud installation directory
NC_ADMIN_USER
a current Nextcloud username that has ADMIN rights
NC_ADMIN_PASSWORD
the password for NC_ADMIN
MIAB_HOSTNAME
the fully-qualified host name of MiaB-LDAP
LDAP_NEXTCLOUD_PASS
supply the password for the LDAP service account Nextcloud
uses to locate and enumerate users and groups. A MiaB-LDAP
installation automatically creates this limited-access service
account with a long random password. Open
/home/user-data/ldap/miab_ldap.conf on your MiaB-LDAP box,
then paste the password for "$LDAP_NEXTCLOUD_DN" as a script
argument. It will be the value of the LDAP_NEXTCLOUD_PASSWORD
key.
SSMTP_ALERTS_EMAIL / SSMTP_AUTH_USER / SSMTP_AUTH_PASS
OPTIONAL. Supplying these arguments will setup ssmtp on your
system and configure it to use MiaB-LDAP as its mail relay.
Email sent with sendmail or ssmtp will be relayed to
MiaB-LDAP. SSMTP_ALERTS_EMAIL is the email address that will
receive messages for all userids less than
1000. SSMTP_AUTH_USER / SSMTP_AUTH_PASS is the email address
that will be used to authenticate with MiaB-LDAP (the sender
or envelope FROM address). You probably want a new/dedicated
email address for this - create a new account in the MiaB-LDAP
admin interface. More information on ssmtp is available at
https://help.ubuntu.com/community/EmailAlerts.
The script must be run as root.
EOF
exit 1
}
if [ "$1" == "-v" ]; then
VERBOSE=1
shift
fi
# Directory where Nextcloud is installed (must contain occ)
NCDIR="$1"
# Nextcloud admin credentials for making user-ldap API calls via curl
NC_ADMIN_USER="$2"
NC_ADMIN_PASSWORD="$3"
# Hostname of the remote MiaB-LDAP
MAILINABOX_HOSTNAME="$4"
# LDAP service account Nextcloud uses to perform ldap searches.
# Values are found in mailinabox:/home/user-data/ldap/miab_ldap.conf
LDAP_NEXTCLOUD_DN="cn=nextcloud,ou=Services,dc=mailinabox"
LDAP_NEXTCLOUD_PASSWORD="$5"
# ssmtp: the person who gets all emails for userids < 1000
SSMTP_ALERTS_EMAIL="$6"
SSMTP_AUTH_USER="$7"
SSMTP_AUTH_PASS="$8"
#
# validate arguments
#
if [ -z "$NCDIR" -o "$1" == "-h" -o "$1" == "--help" ]
then
usage
fi
if [ -z "$NCDIR" -o ! -d "$NCDIR" ]
then
echo "Invalid directory: $NCDIR" 1>&2
exit 1
fi
if [ ! -e "$NCDIR/occ" ]; then
echo "OCC not found at: $NCDIR/occ !" 1>&2
exit 1
fi
if [ -z "$NC_ADMIN_USER" -o \
-z "$MAILINABOX_HOSTNAME" -o \
-z "$LDAP_NEXTCLOUD_PASSWORD" ]
then
usage
fi
if [ ! -z "$SSMTP_ALERTS_EMAIL" ]; then
if [ -z "$(awk -F@ '{print $2}' <<< "$SSMTP_ALERTS_EMAIL")" ]; then
echo "Invalid email address: $SSMTP_ALERTS_EMAIL" 1>&2
exit 1
fi
if [ -z "$(awk -F@ '{print $2}' <<< "$SSMTP_AUTH_USER")" ]; then
echo "Invalid email address: $SSMTP_AUTH_USER" 1>&2
exit 1
fi
fi
if [ -s /etc/mailinabox.conf ]; then
echo "Run on your remote Nextcloud, not on Mail-in-a-Box !!" 1>&2
exit 1
fi
if [ "$EUID" != "0" ]; then
echo "The script must be run as root (sudo)" 1>&2
exit 1
fi
#
# other constants
#
LDAP_URL="ldaps://$MAILINABOX_HOSTNAME"
LDAP_SERVER="$MAILINABOX_HOSTNAME"
LDAP_SERVER_PORT="636"
LDAP_SERVER_STARTTLS="no"
LDAP_BASE="dc=mailinabox"
LDAP_USERS_BASE="ou=Users,dc=mailinabox"
PRIMARY_HOSTNAME="$(hostname --fqdn)"
#
# get the url used to access nextcloud as NC_ADMIN_USER
#
NC_CONFIG_CLI_URL="$(cd "$NCDIR/config"; php -n -r 'include "config.php"; print $CONFIG["overwrite.cli.url"];')"
case "$NC_CONFIG_CLI_URL" in
http:* | https:* )
urlproto=$(awk -F/ '{print $1}' <<< "$NC_CONFIG_CLI_URL")
urlhost=$(awk -F/ '{print $3}' <<< "$NC_CONFIG_CLI_URL")
urlprefix=$(awk -F/ "{ print substr(\$0,length(\"$urlproto\")+length(\"\
$urlhost\")+4) }" <<<"$NC_CONFIG_CLI_URL")
NC_AUTH_URL="$urlproto//${NC_ADMIN_USER}:${NC_ADMIN_PASSWORD}@$urlhost/\
$urlprefix"
;;
* )
NC_AUTH_URL="https://${NC_ADMIN_USER}:${NC_ADMIN_PASSWORD}@$PRIMARY_HOS\
TNAME${NC_CONFIG_CLI_URL:-/}"
;;
esac
say() {
echo "$@"
}
say_verbose() {
if [ $VERBOSE -gt 0 ]; then
echo "$@"
fi
}
die() {
echo "$@" 1>&2
exit 2
}
#
# configure Nextcloud's user-ldap for MiaB-LDAP
#
# See: https://docs.nextcloud.com/server/17/admin_manual/configuration_user/user_auth_ldap_api.html
#
config_user_ldap() {
local id="${1:-s01}"
local first_call="${2:-yes}"
local starttls=0
[ "$LDAP_SERVER_STARTTLS" == "yes" ] && starttls=1
local c=(
"--data-urlencode configData[ldapHost]=$LDAP_URL"
"--data-urlencode configData[ldapPort]=$LDAP_SERVER_PORT"
"--data-urlencode configData[ldapBase]=$LDAP_USERS_BASE"
"--data-urlencode configData[ldapTLS]=$starttls"
"--data-urlencode configData[ldapAgentName]=$LDAP_NEXTCLOUD_DN"
"--data-urlencode configData[ldapAgentPassword]=$LDAP_NEXTCLOUD_PASSWORD"
"--data-urlencode configData[ldapUserDisplayName]=cn"
"--data-urlencode configData[ldapUserDisplayName2]="
"--data-urlencode configData[ldapUserFilter]=(&(objectClass=inetOrgPerson)(objectClass=mailUser))"
"--data-urlencode configData[ldapUserFilterMode]=1"
"--data-urlencode configData[ldapLoginFilter]=(&(objectClass=inetOrgPerson)(objectClass=mailUser)(|(mail=%uid)(uid=%uid)))"
"--data-urlencode configData[ldapEmailAttribute]=mail"
"--data-urlencode configData[ldapGroupFilter]=(objectClass=mailGroup)"
"--data-urlencode configData[ldapGroupMemberAssocAttr]=member"
"--data-urlencode configData[ldapGroupDisplayName]=mail"
"--data-urlencode configData[ldapNestedGroups]=1"
"--data-urlencode configData[turnOnPasswordChange]=1"
"--data-urlencode configData[ldapExpertUsernameAttr]=maildrop"
"--data-urlencode configData[ldapExpertUUIDUserAttr]=uid"
"--data-urlencode configData[ldapExpertUUIDGroupAttr]=entryUUID"
"--data-urlencode configData[ldapConfigurationActive]=1"
)
# apply the settings - note: we can't use localhost because nginx
# will route to the wrong virtual host
local xml
say_verbose "curl \"${NC_AUTH_URL%/}/ocs/v2.php/apps/user_ldap/api/v1/config/$id\""
xml="$(curl -s -S --insecure -X PUT "${NC_AUTH_URL%/}/ocs/v2.php/apps/user_ldap/api/v1/config/$id" -H "OCS-APIREQUEST: true" ${c[@]})"
[ $? -ne 0 ] &&
die "Unable to issue a REST call as $NC_ADMIN_USER to nextcloud. url=$NC_AUTH_URL/ocs/v2.php/apps/user_ldap/api/v1/config/$id"
# did it work?
if [ -z "$xml" ]; then
die "Invalid response from Nextcloud using url '$NC_AUTH_URL'. reponse was '$xml'. Cannot continue."
fi
local statuscode
statuscode=$(python3 -c "import xml.etree.ElementTree as ET; print(ET.fromstring(r'''$xml''').findall('meta')[0].findall('statuscode')[0].text)")
if [ "$statuscode" == "404" -a "$first_call" == "yes" ]; then
# got a 404 so maybe this is the first time -- we have to create
# an initial blank ldap configuration and try again
xml="$(curl -s -S --insecure -X POST "${NC_AUTH_URL%/}/ocs/v2.php/apps/user_ldap/api/v1/config" -H "OCS-APIREQUEST: true")"
[ $? -ne 0 ] &&
die "Unable to issue a REST call as $NC_ADMIN_USER to nextcloud: $xml"
statuscode=$(python3 -c "import xml.etree.ElementTree as ET; print(ET.fromstring(r'''$xml''').findall('meta')[0].findall('statuscode')[0].text)")
[ $? -ne 0 -o "$statuscode" != "200" ] &&
die "Error creating initial ldap configuration: $xml"
id=$(python3 -c "import xml.etree.ElementTree as ET; print(ET.fromstring(r'''$xml''').findall('data')[0].findall('configID')[0].text)" 2>/dev/null)
[ $? -ne 0 ] &&
die "Error creating initial ldap configuration: $xml"
config_user_ldap "$id" no
elif [ "$statuscode" == "997" -a "$first_call" == "yes" ]; then
# could not log in
die "Could not authenticate as $NC_ADMIN_USER to perform user-ldap API call. statuscode=$statuscode: $xml"
elif [ "$statuscode" != "200" ]; then
die "Unable to apply ldap configuration to nextcloud: id=$id first_call=$first_call statuscode=$statuscode: $xml"
fi
return 0
}
enable_user_ldap() {
# install prerequisite package php-ldap
# if using Docker Hub's php image, don't install at all
if [ ! -e /etc/apt/preferences.d/no-debian-php ]; then
say_verbose "Installing system package php-ldap"
apt-get install -y -qq php-ldap || die "Could not install php-ldap package"
#restart_service php7.2-fpm
fi
# enable user_ldap
if [ ! -x /usr/bin/sudo ]; then
say "WARNING: sudo is not installed: Unable to run occ to check and/or enable Nextcloud app \"user-ldap\"."
else
say_verbose "Enable user-ldap"
sudo -E -u www-data php $NCDIR/occ app:enable user_ldap -q
[ $? -ne 0 ] && die "Unable to enable user_ldap nextcloud app"
fi
}
install_app() {
local app="$1"
if [ ! -x /usr/bin/sudo ]; then
say "WARNING: sudo is not installed: Unable to run occ to check and/or install Nextcloud app \"$app\"."
elif [ -z "$(sudo -E -u www-data php $NCDIR/occ app:list | grep $app)" ]; then
say_verbose "Install app '$app'"
sudo -E -u www-data php $NCDIR/occ app:install $app
[ $? -ne 0 ] && die "Unable to install Nextcloud app '$app'"
fi
}
setup_ssmtp() {
# sendmail-like mailer with a mailhub to remote mail-in-a-box
# see: https://help.ubuntu.com/community/EmailAlerts
if [ "$(. /etc/os-release; echo $NAME)" != "Ubuntu" ]; then
die "Sorry, ssmtp is only supported on Ubuntu"
fi
say_verbose "Installing system package ssmtp"
apt-get install -y -qq ssmtp
if [ ! -e /etc/ssmtp/ssmtp.conf.orig ]; then
cp /etc/ssmtp/ssmtp.conf /etc/ssmtp/ssmtp.conf.orig
fi
cat <<EOF >/etc/ssmtp/ssmtp.conf
# Generated by MiaB-LDAP integration script on $(date)
# The person who gets all mail for userids < 1000
root=${SSMTP_ALERTS_EMAIL}
# The place where mail goes
mailhub=${MAILINABOX_HOSTNAME}:587
AuthUser=${SSMTP_AUTH_USER}
AuthPass=${SSMTP_AUTH_PASS}
UseTLS=YES
UseSTARTTLS=YES
# The full hostname
hostname=${PRIMARY_HOSTNAME}
# Are users allowed to set their own From address?
FromLineOverride=YES
EOF
}
remote_mailinabox_handler() {
say_verbose "Installing system package ldap-utils"
apt-get install -y -qq ldap-utils python3 || die "Could not install required packages"
local count=0
local ldap_debug=""
while /bin/true; do
# ensure we can search
local output
say ""
say "Testing MiaB-LDAP connection..."
output="$(ldapsearch $ldap_debug -v -H $LDAP_URL -x -D "$LDAP_NEXTCLOUD_DN" -w "$LDAP_NEXTCLOUD_PASSWORD" -b "$LDAP_BASE" -s base 2>&1)"
local code=$?
if [ $code -ne 0 ]; then
say "Unable to contact $LDAP_URL"
say " base=$LDAP_BASE"
say " user=$LDAP_NEXTCLOUD_DN"
say " error code=$code"
say " msg= $output"
say ""
say "You may need to permit access to the ldap server running on $LDAP_SERVER"
say "On $LDAP_SERVER execute:"
local ip
for ip in $(hostname -I); do
say " \$ ufw allow proto tcp from $ip to any port ldaps"
done
say ""
let count+=1
if [ $count -gt 5 ]; then
die "Giving up"
fi
read -p "Press [enter] when ready, or \"no\" to quit: " ans
[ "$ans" == "no" ] && die "Quit"
ldap_debug="-d 9"
else
say "Test successful - able to bind and search as $LDAP_NEXTCLOUD_DN"
break
fi
done
enable_user_ldap
config_user_ldap
return 0
}
echo "Integrating Nextcloud with Mail-in-a-box LDAP"
remote_mailinabox_handler
# contacts and calendar are required for Roundcube and Z-Push
install_app "calendar"
install_app "contacts"
if [ ! -z "${SSMTP_ALERTS_EMAIL}" ]; then
setup_ssmtp
fi
say ""
say "Done!"

View File

@ -0,0 +1,161 @@
#!/bin/bash
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# maintain a separate conf file because setup rewrites mailinabox.conf
touch /etc/mailinabox_mods.conf
. /etc/mailinabox_mods.conf
# where webmail.sh installs roundcube
RCM_DIR=/usr/local/lib/roundcubemail
RCM_PLUGIN_DIR=${RCM_DIR}/plugins
# where zpush.sh installs z-push
ZPUSH_DIR=/usr/local/lib/z-push
configure_zpush() {
# have zpush use the remote nextcloud for carddav/caldav
# instead of the nextcloud that comes with mail-in-a-box
cp setup/mods.available/conf/zpush/backend_carddav.php $ZPUSH_DIR/backend/carddav/config.php
cp setup/mods.available/conf/zpush/backend_caldav.php $ZPUSH_DIR/backend/caldav/config.php
local var val
for var in NC_PROTO NC_HOST NC_PORT NC_PREFIX; do
eval "val=\$$var"
sed -i "s^$var^${val%/}^g" $ZPUSH_DIR/backend/carddav/config.php
sed -i "s^$var^${val%/}^g" $ZPUSH_DIR/backend/caldav/config.php
done
}
configure_roundcube() {
# replace the plugin configuration from the default Mail-In-A-Box
local name="${1:-$NC_HOST}"
local baseurl="$NC_PROTO://$NC_HOST:$NC_PORT$NC_PREFIX"
# Configure CardDav plugin
#
# 1. make MiaB ownCloud contacts read-only so users can still
# access them, but not change them, and no sync occurs
#
# a. set 'active' to 'false'
# regular expression before "bashing" it:
# (['"]active['"][ \t]*=>[ \t]*)true
#
sed -i 's/\(['"'"'"]active['"'"'"][ \t]*=>[ \t]*\)true/\1false/' ${RCM_PLUGIN_DIR}/carddav/config.inc.php
# b. set 'readonly' to 'true'
# regular expressions is like above
sed -i 's/\(['"'"'"]readonly['"'"'"][ \t]*=>[ \t]*\)false/\1true/' ${RCM_PLUGIN_DIR}/carddav/config.inc.php
#
# 2. add the remote Nextcloud
#
cat >> ${RCM_PLUGIN_DIR}/carddav/config.inc.php <<EOF
<?php
/* Do not edit. Written by Mail-in-a-Box-LDAP. Regenerated on updates. */
//\$prefs['_GLOBAL']['hide_preferences'] = true;
//\$prefs['_GLOBAL']['suppress_version_warning'] = true;
\$prefs['cloud'] = array(
'name' => '$name',
'username' => '%u', // login username
'password' => '%p', // login password
'url' => '${baseurl%/}/remote.php/carddav/addressbooks/%u/contacts',
'active' => true,
'readonly' => false,
'refresh_time' => '02:00:00',
'fixed' => array('username','password'),
'preemptive_auth' => '1',
'hide' => false,
);
?>
EOF
}
remote_nextcloud_handler() {
echo ""
echo "============================"
echo "Configure a remote Nextcloud"
echo "============================"
echo 'Enter the url or hostname and web prefix of your remote Nextcloud'
echo 'For example:'
echo ' "cloud.mydomain.com/" - Nextcloud server with no prefix'
echo ' "cloud.mydomain.com" - same as above'
echo ' "www.mydomain.com/cloud" - a Nextcloud server having a prefix /cloud'
echo ''
local ans
local current_url=""
if [ -z "${NC_HOST:-}" ]; then
if [ -z "${NONINTERACTIVE:-}" ]; then
read -p "[your Nextcloud's hostname/prefix] " ans
fi
[ -z "$ans" ] && return 0
else
current_url="$NC_PROTO://$NC_HOST:$NC_PORT$NC_PREFIX"
if [ -z "${NONINTERACTIVE:-}" ]; then
read -p "[$current_url] " ans
if [ -z "$ans" ]; then
ans="$current_url"
elif [ "$ans" == "none" ]; then
ans=""
fi
else
ans="$current_url"
fi
fi
case "$ans" in
https://* )
NC_PROTO="https"
NC_PORT="443"
ans="$(awk -F: '{print substr($0,9)}' <<< "$ans")"
;;
http://* )
NC_PROTO="http"
NC_PORT="80"
ans="$(awk -F: '{print substr($0,8)}' <<< "$ans")"
;;
* )
NC_PROTO="https"
NC_PORT="443"
;;
esac
NC_PREFIX="/$(awk -F/ '{print substr($0,length($1)+2)}' <<< "$ans")"
NC_HOST="$(awk -F/ '{print $1}' <<< "$ans")"
if grep ":" <<< "$NC_HOST" >/dev/null; then
NC_PORT="$(awk -F: '{print $2}' <<< "$NC_HOST")"
NC_HOST="$(awk -F: '{print $1}' <<< "$NC_HOST")"
fi
local new_url="$NC_PROTO://$NC_HOST:$NC_PORT$NC_PREFIX"
if [ ! -z "$NC_HOST" ]; then
echo "Using Nextcloud ${new_url}"
# configure roundcube contacts
configure_roundcube "$NC_HOST"
# configure zpush (which links to contacts & calendar)
configure_zpush
# prevent nginx from serving any miab-installed nextcloud files
chmod 000 /usr/local/lib/owncloud
fi
tools/editconf.py /etc/mailinabox_mods.conf \
"NC_PROTO=$NC_PROTO" \
"NC_HOST=$NC_HOST" \
"NC_PORT=$NC_PORT" \
"NC_PREFIX=$NC_PREFIX"
}
remote_nextcloud_handler

View File

@ -1,6 +1,7 @@
#!/bin/bash
# Munin: resource monitoring tool
#################################################
[ "${FEATURE_MUNIN:-true}" == "false" ] && return 0
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars

View File

@ -1,6 +1,7 @@
#!/bin/bash
# Nextcloud
##########################
[ "${FEATURE_NEXTCLOUD:-true}" == "false" ] && return 0
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
@ -100,6 +101,10 @@ nextcloud_hash=50b98d2c2f18510b9530e558ced9ab51eb4f11b0
# version.php since the restore procedure can leave the system in a state where you have a newer Nextcloud
# application version than the database.
# ensure directory is accessible
if [ -d "/usr/local/lib/owncloud" ]; then
chmod u+rx /usr/local/lib/owncloud
fi
# If config.php exists, get version number, otherwise CURRENT_NEXTCLOUD_VER is empty.
if [ -f "$STORAGE_ROOT/owncloud/config.php" ]; then
CURRENT_NEXTCLOUD_VER=$(php -r "include(\"$STORAGE_ROOT/owncloud/config.php\"); echo(\$CONFIG['version']);")

View File

@ -97,11 +97,6 @@ if [ ! -s $STORAGE_ROOT/ssl/ssl_private_key.pem ]; then
(umask 037; hide_output \
openssl genrsa -out $STORAGE_ROOT/ssl/ssl_private_key.pem 2048)
# Give the group 'ssl-cert' read access so slapd can read it
groupadd -fr ssl-cert
chgrp ssl-cert $STORAGE_ROOT/ssl/ssl_private_key.pem
chmod g+r $STORAGE_ROOT/ssl/ssl_private_key.pem
# Remove the ssl_certificate.pem symbolic link to force a
# regeneration of the server certificate. It needs to be
# signed by the new ca.
@ -110,6 +105,11 @@ if [ ! -s $STORAGE_ROOT/ssl/ssl_private_key.pem ]; then
fi
fi
# Give the group 'ssl-cert' read access so slapd can read it
groupadd -fr ssl-cert
chgrp ssl-cert $STORAGE_ROOT/ssl/ssl_private_key.pem
chmod g+r $STORAGE_ROOT/ssl/ssl_private_key.pem
#
# Generate a root CA certificate
#
@ -123,15 +123,21 @@ if [ ! -f $STORAGE_ROOT/ssl/ca_certificate.pem ]; then
-passin 'pass:SECRET-PASSWORD' \
-out $CERT \
-subj '/CN=Temporary-Mail-In-A-Box-CA'
fi
# add the certificate to the system's trusted root ca list
if [ ! -e /usr/local/share/ca-certificates/mailinabox.crt ]; then
# add the CA certificate to the system's trusted root ca list
# this is required for openldap's TLS implementation
# do this as a separate step in case a CA certificate is manually
# copied onto the machine for QA/test
CERT=$STORAGE_ROOT/ssl/ca_certificate.pem
hide_output \
cp $CERT /usr/local/share/ca-certificates/mailinabox.crt
hide_output \
update-ca-certificates
fi
# Generate a signed SSL certificate because things like nginx, dovecot,
# etc. won't even start without some certificate in place, and we need nginx
# so we can offer the user a control panel to install a better certificate.

View File

@ -147,6 +147,15 @@ echo
certbot register --register-unsafely-without-email --agree-tos --config-dir $STORAGE_ROOT/ssl/lets_encrypt
fi
#
# Run setup mods
#
if [ -d "${LOCAL_MODS_DIR:-local}" ]; then
for mod in $(ls "${LOCAL_MODS_DIR:-local}" | grep -v '~$'); do
${LOCAL_MODS_DIR:-local}/$mod
done
fi
# Done.
echo
echo "-----------------------------------------------"

View File

@ -256,7 +256,7 @@ if [ -z "${DISABLE_FIREWALL:-}" ]; then
apt_install ufw
# Allow incoming connections to SSH.
ufw_allow ssh;
ufw_limit ssh;
# ssh might be running on an alternate port. Use sshd -T to dump sshd's #NODOC
# settings, find the port it is supposedly running on, and open that port #NODOC
@ -266,7 +266,7 @@ if [ -z "${DISABLE_FIREWALL:-}" ]; then
if [ "$SSH_PORT" != "22" ]; then
echo Opening alternate SSH port $SSH_PORT. #NODOC
ufw_allow $SSH_PORT #NODOC
ufw_limit $SSH_PORT #NODOC
fi
fi

View File

@ -29,8 +29,8 @@ apt_install \
# Install Roundcube from source if it is not already present or if it is out of date.
# Combine the Roundcube version number with the commit hash of plugins to track
# whether we have the latest version of everything.
VERSION=1.4.4
HASH=4e425263f5bec27d39c07bde524f421bda205c07
VERSION=1.4.6
HASH=44961ef62bb9c9875141ca34704bbc7d6f36373d
PERSISTENT_LOGIN_VERSION=6b3fc450cae23ccb2f393d0ef67aa319e877e435
HTML5_NOTIFIER_VERSION=4b370e3cd60dabd2f428a26f45b677ad1b7118d5
CARDDAV_VERSION=3.0.3

View File

@ -0,0 +1,156 @@
#!/usr/bin/env php
<?php
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require_once INSTALL_PATH.'program/include/clisetup.php';
ini_set('memory_limit', -1);
function usage()
{
print "Usage: carddav_refresh.sh [-id <number>] username password\n";
print "Force a sync of a user's addressbook with the remote server\n";
print "Place this script in /path/to/roundcubemail/bin, then change the working directory to /path/to/roundcubemail, then run ./bin/cardav_refresh.sh";
exit(1);
}
function _die($msg)
{
fwrite(STDERR, $msg . "\n");
exit(1);
}
$args = rcube_utils::get_opt(array('id' => 'dbid'));
$dbid = 0;
if (!empty($args['dbid'])) {
$dbid = intval($args['dbid']);
}
$username = trim($args[0]);
if (empty($username)) {
print "Missing username";
usage();
}
$password = trim($args[1]);
if (empty($password)) {
usage();
}
// -----
// From index.php -- initialization and login
// -----
// init application, start session, init output class, etc.
$RCMAIL = rcmail::get_instance(0, $GLOBALS['env']);
// trigger startup plugin hook
$startup = $RCMAIL->plugins->exec_hook('startup', array('task' => $RCMAIL->task, 'action' => $RCMAIL->action));
$RCMAIL->set_task($startup['task']);
$RCMAIL->action = $startup['action'];
$auth = $RCMAIL->plugins->exec_hook('authenticate', array(
'host' => $RCMAIL->autoselect_host(),
'user' => $username,
'pass' => $password,
'valid' => true,
'cookiecheck' => false,));
// Login
if ($auth['valid'] && !$auth['abort']
&& $RCMAIL->login($auth['user'], $auth['pass'], $auth['host'], $auth['cookiecheck']))
{
print "login ok\n";
}
else
{
_die("login failed");
}
// ----------------------------------------------------
// Get the user id (see deluser.sh)
// ----------------------------------------------------
$host = $auth['host']; # can be a url (eg: ssl://localhost)
$host_url = parse_url($host);
if ($host_url['host']) {
$host = $host_url['host'];
}
$user = rcube_user::query($auth['user'], $host);
if (!$user) {
_die("User not found auth[host]=" . $auth['host'] . " host=" . $host . "\n");
}
// ----------------------------------------------------
// ensure the carddav tables are created and populated
// ----------------------------------------------------
require_once('plugins/carddav/carddav_backend.php');
require_once('plugins/carddav/carddav.php');
try {
$c = new carddav(rcube_plugin_api::get_instance());
$c->task .= "|cli";
$c->init();
print "done: init\n";
// this ensures the carddav tables are created
$c->checkMigrations();
print "done: init tables\n";
// this populates carddav_addressbooks from config
$c->init_presets();
print "done: init addressbooks\n";
} catch(exception $e) {
print $e . "\n";
_die("failed");
}
// -------------------------------------------------------------
// Set the last_updated field for addressbooks to an old date.
// That will force a sync/update
// -------------------------------------------------------------
$db = $rcmail->get_dbh();
$db->db_connect('w');
if (!$db->is_connected() || $db->is_error()) {
_die("No DB connection\n" . $db->is_error());
}
print "db connected\n";
$db->query("update " . $db->table_name('carddav_addressbooks') . " set last_updated=? WHERE active=1 and user_id=" . $user->ID, '2000-01-01 00:00:00');
print "update made\n";
if ($db->is_error()) {
_die("DB error occurred: " . $db->is_error());
}
// ------------------------------------------------------
// Update/sync all out-of-date address books
// ------------------------------------------------------
// first get all row ids
$dbid=array();
$sql_result = $db->query('SELECT id FROM ' .
$db->table_name('carddav_addressbooks') .
' WHERE active=1');
if ($db->is_error()) {
_die("DB error occurred: " . $db->is_error());
}
while ($row = $db->fetch_assoc($sql_result)) {
array_push($dbid, intval($row['id']));
print "carddav_addressbooks id: " . $row['id'] . "\n";
}
// instantiating carddav_backend causes the update/sync
foreach($dbid as $id) {
$config = carddav_backend::carddavconfig($id);
if ($config['needs_update']) {
print "instantiating carddav_backend: " . $id . "\n";
$b = new carddav_backend($id);
print("success\n");
}
}

19
tests/lib/all.sh Normal file
View File

@ -0,0 +1,19 @@
#
# source all lib scripts
#
# from your script, supply the path to this directory as the first argument
#
# eg source "tests/lib/all.sh" "tests/lib"
#
# failure to load any script is fatal!
. "$1/color-output.sh" || exit 1
. "$1/locations.sh" || exit 2
. "$1/misc.sh" || exit 3
. "$1/rest.sh" || exit 4
. "$1/system.sh" || exit 5
. "$1/carddav.sh" || exit 6
. "$1/populate.sh" || exit 7
. "$1/installed-state.sh" || exit 8

280
tests/lib/carddav.sh Normal file
View File

@ -0,0 +1,280 @@
#
# requires:
# system packages: [ curl, python3, sqlite3 ]
# scripts: [ color-output.sh, misc.sh, locations.sh ]
#
# ASSETS_DIR: where the assets directory is located (defaults to
# tests/assets)
#
nextcloud_url() {
# eg: http://localhost/cloud/
carddav_url | sed 's|\(.*\)/remote.php/.*|\1/|'
}
carddav_url() {
# get the carddav url as configured in z-push for the user specified
# eg: http://localhost/cloud/remote.php/dav/addressbooks/users/admin/contacts/
local user="${1:-%u}"
local path="${2:-CARDDAV_DEFAULT_PATH}"
local php="include \"$ZPUSH_DIR/backend/carddav/config.php\"; print CARDDAV_PROTOCOL . \"://\" . CARDDAV_SERVER . \":\" . CARDDAV_PORT . "
php="$php$path;"
local url
url="$(php -n -r "$php")"
[ $? -ne 0 ] && die "Unable to run php to extract carddav url from z-push"
sed "s/%u/$user/" <<< "$url"
}
carddav_rest() {
# issue a CardDAV rest call to Nextcloud
# SEE: https://tools.ietf.org/html/rfc6352
#
# The function will set the following global variables regardless
# of exit code:
# REST_HTTP_CODE
# REST_OUTPUT
# REST_ERROR
# REST_ERROR_BRIEF
#
# Return values:
# 0 indicates success (curl returned 0 or a code deemed to be
# successful and HTTP status is >=200 but <300)
# 1 curl returned with non-zero code that indicates and error
# 2 the response status was <200 or >= 300
#
# Debug messages are sent to stderr
#
local verb="$1"
local uri="$2"
local auth_user="$3"
local auth_pass="$4"
shift; shift; shift; shift # remaining arguments are data
local url
case "$uri" in
/* )
url="$(nextcloud_url)${uri#/}"
;;
http* )
url="$uri"
;;
* )
url="$(carddav_url "$auth_user")${uri#/}"
;;
esac
local data=()
local item output onlydata="false"
for item; do
case "$item" in
-- )
onlydata="true"
;;
--* )
# curl argument
if $onlydata; then
data+=("--data" "$item");
else
data+=("$item")
fi
;;
* )
onlydata="true"
data+=("--data" "$item");
;;
esac
done
local ct
case "${data[1]}" in
BEGIN:VCARD* )
ct="text/vcard"
;;
* )
ct='text/xml; charset="utf-8"'
esac
local tmp1="/tmp/curl.$$.tmp"
echo "spawn: curl -w \"%{http_code}\" -X $verb -H 'Content-Type: $ct' --user \"${auth_user}:xxx\" ${data[@]} \"$url\"" 1>&2
output=$(curl -s -S -w "%{http_code}" -X $verb -H "Content-Type: $ct" --user "${auth_user}:${auth_pass}" "${data[@]}" "$url" 2>$tmp1)
local code=$?
# http status is last 3 characters of output, extract it
REST_HTTP_CODE=$(awk '{S=substr($0,length($0)-2)} END {print S}' <<<"$output")
REST_OUTPUT=$(awk 'BEGIN{L=""}{ if(L!="") print L; L=$0 } END { print substr(L,1,length(L)-3) }' <<<"$output")
REST_ERROR=""
REST_ERROR_BRIEF=""
[ -z "$REST_HTTP_CODE" ] && REST_HTTP_CODE="000"
if [ $code -ne 0 -o \
$REST_HTTP_CODE -lt 200 -o \
$REST_HTTP_CODE -ge 300 ]
then
if [ $code -ne 0 -a "$REST_HTTP_CODE" == "000" ]; then
REST_ERROR="exit code $code"
REST_ERROR_BRIEF="$REST_ERROR"
else
REST_ERROR="REST status $REST_HTTP_CODE: $REST_OUTPUT"
REST_ERROR_BRIEF=$(python3 -c "import xml.etree.ElementTree as ET; print(ET.fromstring(r'''$REST_OUTPUT''').find('s:message',{'s':'http://sabredav.org/ns'}).text)" 2>/dev/null)
if [ -z "$REST_ERROR_BRIEF" ]; then
REST_ERROR_BRIEF="$REST_ERROR"
else
REST_ERROR_BRIEF="$REST_HTTP_CODE: $REST_ERROR_BRIEF"
fi
if [ $code -ne 0 ]; then
REST_ERROR_BRIEF="exit code $code: $REST_ERROR_BRIEF"
REST_ERROR="exit code $code: $REST_ERROR"
fi
fi
if [ -s $tmp1 ]; then
REST_ERROR="$REST_ERROR: $(cat $tmp1)"
REST_ERROR_BRIEF="$REST_ERROR_BRIEF: $(cat $tmp1)"
fi
rm -f $tmp1
echo "${F_DANGER}$REST_ERROR${F_RESET}" 1>&2
[ $code -ne 0 ] && return 1
return 2
fi
echo "CURL succeded, HTTP status $REST_HTTP_CODE" 1>&2
echo "$output" 1>&2
rm -f $tmp1
return 0
}
carddav_ls() {
# place all .vcf files into global FILES
# debug messages are sent to stderr
local user="$1"
local pass="$2"
shift; shift
FILES=()
if ! carddav_rest PROPFIND "" "$user" "$pass" $@
then
return 1
fi
FILES=( $(python3 -c "import xml.etree.ElementTree as ET; [print(el.find('d:href',{'d':'DAV:'}).text) for el in ET.fromstring(r'''$REST_OUTPUT''').findall('d:response',{'d':'DAV:'}) if el.find('d:href',{'d':'DAV:'}) is not None]") )
local idx=${#FILES[*]}
let idx-=1
while [ $idx -ge 0 ]; do
# remove non .vcf entries, take basename contact href
case "${FILES[$idx]}" in
*.vcf )
FILES[$idx]=$(basename "${FILES[$idx]}")
;;
* )
unset "FILES[$idx]"
;;
esac
let idx-=1
done
}
carddav_make_addressbook() {
local user="$1"
local pass="$2"
local name="$3"
local desc="${4:-$name}"
local xml="<?xml version=\"1.0\" encoding=\"utf-8\" ?>
<D:mkcol xmlns:D=\"DAV:\"
xmlns:C=\"urn:ietf:params:xml:ns:carddav\">
<D:set>
<D:prop>
<D:resourcetype>
<D:collection/>
<C:addressbook/>
</D:resourcetype>
<D:displayname>$name</D:displayname>
<C:addressbook-description xml:lang=\"en\">$desc</C:addressbook-description>
</D:prop>
</D:set>
</D:mkcol>"
local url="$(carddav_url "$user" CARDDAV_PATH)"
carddav_rest MKCOL "$url" "$user" "$pass" "$xml"
}
carddav_add_contact() {
# debug messages are sent to stderr
local user="$1"
local pass="$2"
local c_name="$3"
local c_phone="$4"
local c_email="$5"
local c_uid="${6:-$(generate_uuid)}"
shift; shift; shift; shift; shift; shift
local vcard="BEGIN:VCARD
VERSION:3.0
UID:$c_uid
REV;VALUE=DATE-AND-OR-TIME:$(date -u +%Y%m%dT%H%M%SZ)
FN:$c_name
EMAIL;TYPE=INTERNET,PREF:$c_email
NOTE:Miab-LDAP QA
ORG:Miab-LDAP
TEL;TYPE=WORK,VOICE:$c_phone
END:VCARD"
carddav_rest PUT "$c_uid.vcf" "$user" "$pass" $@ -- "$vcard"
}
carddav_delete_contact() {
local user="$1"
local pass="$2"
local c_uid="$3"
shift; shift; shift
carddav_rest DELETE "$c_uid.vcf" "$user" "$pass" $@
}
roundcube_force_carddav_refresh() {
local user="$1"
local pass="$2"
local assets_dir="${ASSETS_DIR:-tests/assets}"
local code
if ! cp "$assets_dir/mail/roundcube/carddav_refresh.sh" $RCM_DIR/bin
then
return 1
fi
pushd "$RCM_DIR" >/dev/null
bin/carddav_refresh.sh "$user" "$pass"
code=$?
popd >/dev/null
return $code
}
roundcube_carddav_contact_exists() {
# returns 0 if contact exists
# 1 if contact does not exist
# 2 if an error occurred
# stderr receives error messages
local user="$1"
local pass="$2"
local c_uid="$3"
local db="${4:-$STORAGE_ROOT/mail/roundcube/roundcube.sqlite}"
local output
output="$(sqlite3 "$db" "select name from carddav_contacts where cuid='$c_uid'")"
[ $? -ne 0 ] && return 2
if [ -z "$output" ]; then
return 1
else
return 0
fi
}
roundcube_dump_contacts() {
local db="${1:-$STORAGE_ROOT/mail/roundcube/roundcube.sqlite}"
local cols="${2:-name,cuid}"
sqlite3 "$db" "select $cols FROM carddav_contacts"
}

64
tests/lib/color-output.sh Normal file
View File

@ -0,0 +1,64 @@
# ansi escapes for hilighting text
F_DANGER=$(echo -e "\033[31m")
F_WARN=$(echo -e "\033[93m")
F_SUCCESS=$(echo -e "\033[32m")
F_RESET=$(echo -e "\033[39m")
success() {
local echoarg
case "$1" in
-n )
echoarg="$1"
shift
;;
* )
echoarg=""
esac
echo $echoarg "${F_SUCCESS}$1${F_RESET}"
}
danger() {
local echoarg
case "$1" in
-n )
echoarg="$1"
shift
;;
* )
echoarg=""
esac
echo $echoarg "${F_DANGER}$1${F_RESET}"
}
warn() {
local echoarg
case "$1" in
-n )
echoarg="$1"
shift
;;
* )
echoarg=""
esac
echo $echoarg "${F_WARN}$1${F_RESET}"
}
H1() {
local msg="$1"
echo "----------------------------------------------"
if [ ! -z "$msg" ]; then
echo " $msg"
echo "----------------------------------------------"
fi
}
H2() {
local msg="$1"
if [ -z "$msg" ]; then
echo "***"
else
echo "*** $msg ***"
fi
}

View File

@ -0,0 +1,141 @@
#
# requires:
# scripts: [ colored-output.sh, rest.sh ]
#
# these functions are meant for comparing upstream (non-LDAP)
# installations to a subsequent MiaB-LDAP upgrade
#
installed_state_capture() {
# users and aliases
# dns zone files
# TOOD: tls certificates: expected CN's
local state_dir="$1"
local info="$state_dir/info.txt"
H1 "Capture installed state to $state_dir"
# nuke saved state, if any
rm -rf "$state_dir"
mkdir -p "$state_dir"
# create info.json
H2 "create info.txt"
echo "STATE_VERSION=1" > "$info"
echo "GIT_VERSION='$(git describe --abbrev=0)'" >>"$info"
echo "MIGRATION_VERSION=$(cat "$STORAGE_ROOT/mailinabox.version")" >>"$info"
# record users
H2 "record users"
if ! rest_urlencoded GET "/admin/mail/users?format=json" "$EMAIL_ADDR" "$EMAIL_PW" --insecure 2>/dev/null
then
echo "Unable to get users: rc=$? err=$REST_ERROR" 1>&2
return 1
fi
echo "$REST_OUTPUT" > "$state_dir/users.json"
# record aliases
H2 "record aliases"
if ! rest_urlencoded GET "/admin/mail/aliases?format=json" "$EMAIL_ADDR" "$EMAIL_PW" --insecure 2>/dev/null
then
echo "Unable to get aliases: rc=$? err=$REST_ERROR" 1>&2
return 2
fi
echo "$REST_OUTPUT" > "$state_dir/aliases.json"
# record dns config
H2 "record dns details"
local file
mkdir -p "$state_dir/zones"
for file in /etc/nsd/zones/*.signed; do
if ! cp "$file" "$state_dir/zones"
then
echo "Copy $file -> $state_dir/zones failed" 1>&2
return 3
fi
done
return 0
}
installed_state_compare() {
local s1="$1"
local s2="$2"
local output
local changed="false"
H1 "COMPARE STATES: $(basename "$s1") VS $(basename "$2")"
H2 "Users"
# users
output="$(diff "$s1/users.json" "$s2/users.json" 2>&1)"
if [ $? -ne 0 ]; then
changed="true"
echo "USERS ARE DIFFERENT!"
echo "$output"
else
echo "No change"
fi
H2 "Aliases"
output="$(diff "$s1/aliases.json" "$s2/aliases.json" 2>&1)"
if [ $? -ne 0 ]; then
change="true"
echo "ALIASES ARE DIFFERENT!"
echo "$output"
else
echo "No change"
fi
H2 "DNS - zones missing"
local zone count=0
for zone in $(cd "$s1/zones"; ls *.signed); do
if [ ! -e "$s2/zones/$zone" ]; then
echo "MISSING zone: $zone"
changed="true"
let count+=1
fi
done
echo "$count missing"
H2 "DNS - zones added"
count=0
for zone in $(cd "$s2/zones"; ls *.signed); do
if [ ! -e "$s2/zones/$zone" ]; then
echo "ADDED zone: $zone"
changed="true"
let count+=1
fi
done
echo "$count added"
H2 "DNS - zones changed"
count=0
for zone in $(cd "$s1/zones"; ls *.signed); do
if [ -e "$s2/zones/$zone" ]; then
# all the signatures change if we're using self-signed certs
local t1="/tmp/s1.$$.txt"
local t2="/tmp/s2.$$.txt"
awk '$4 == "RRSIG" || $4 == "NSEC3" { next; } $4 == "SOA" { print $1" "$2" "$3" "$4" "$5" "$6" "$8" "$9" "$10" "$11" "$12; next } { print $0 }' "$s1/zones/$zone" > "$t1"
awk '$4 == "RRSIG" || $4 == "NSEC3" { next; } $4 == "SOA" { print $1" "$2" "$3" "$4" "$5" "$6" "$8" "$9" "$10" "$11" "$12; next } { print $0 }' "$s2/zones/$zone" > "$t2"
output="$(diff "$t1" "$t2" 2>&1)"
if [ $? -ne 0 ]; then
echo "CHANGED zone: $zone"
echo "$output"
changed="true"
let count+=1
fi
fi
done
echo "$count zone files had differences"
if $changed; then
return 1
else
return 0
fi
}

8
tests/lib/locations.sh Normal file
View File

@ -0,0 +1,8 @@
#
# where webmail.sh installs roundcube
RCM_DIR=/usr/local/lib/roundcubemail
RCM_PLUGIN_DIR=${RCM_DIR}/plugins
# where zpush.sh installs z-push
ZPUSH_DIR=/usr/local/lib/z-push

83
tests/lib/misc.sh Normal file
View File

@ -0,0 +1,83 @@
#
# misc helpful functions
#
# requirements:
# system packages: [ python3 ]
array_contains() {
local searchfor="$1"
shift
local item
for item; do
[ "$item" == "$searchfor" ] && return 0
done
return 1
}
is_true() {
# empty string is not true
if [ "$1" == "true" \
-o "$1" == "TRUE" \
-o "$1" == "True" \
-o "$1" == "yes" \
-o "$1" == "YES" \
-o "$1" == "Yes" \
-o "$1" == "1" ]
then
return 0
else
return 1
fi
}
is_false() {
if is_true $@; then return 1; fi
return 0
}
email_localpart() {
local addr="$1"
awk -F@ '{print $1}' <<<"$addr"
}
email_domainpart() {
local addr="$1"
awk -F@ '{print $2}' <<<"$addr"
}
generate_uuid() {
local uuid
uuid=$(python3 -c "import uuid; print(uuid.uuid4())")
[ $? -ne 0 ] && die "Unable to generate a uuid"
echo "$uuid"
}
generate_qa_password() {
echo "Test$(date +%s)"
}
static_qa_password() {
echo "Test_1234"
}
sha1() {
local txt="$1"
python3 -c "import hashlib; m=hashlib.sha1(); m.update(bytearray(r'''$txt''','utf-8')); print(m.hexdigest());" || die "Unable to generate sha1 hash"
}
elapsed_pretty() {
local start_s="$1"
local end_s="$2"
local elapsed elapsed_m elapsed_s
if [ -z "$end_s" ]; then
elapsed="$start_s"
else
let elapsed="$end_s - $start_s"
fi
let elapsed_m="$elapsed / 60"
let elapsed_s="$elapsed % 60"
echo "${elapsed_m}m ${elapsed_s}s"
}

99
tests/lib/populate.sh Normal file
View File

@ -0,0 +1,99 @@
#
# requires:
# scripts: [ rest.sh, misc.sh ]
#
populate_miab_users() {
local url="$1"
local admin_email="${2:-$EMAIL_ADDR}"
local admin_pass="${3:-$EMAIL_PW}"
shift; shift; shift # remaining arguments are users to add
# each "user" argument is in the format "email:password"
# if no password is given a "qa" password will be generated
[ $# -eq 0 ] && return 0
#
# get the existing users
#
local current_users=() user
if ! rest_urlencoded GET ${url%/}/admin/mail/users "$admin_email" "$admin_pass" --insecure 2>/dev/null; then
echo "Unable to enumerate users: rc=$? err=$REST_ERROR" 1>&2
return 1
fi
for user in $REST_OUTPUT; do
current_users+=("$user")
done
#
# add the new users
#
local pw="$(generate_qa_password)"
for user; do
local user_email="$(awk -F: '{print $1}' <<< "$user")"
local user_pass="$(awk -F: '{print $2}' <<< "$user")"
if array_contains "$user_email" "${current_users[@]}"; then
echo "Not adding user $user_email: already exists"
elif ! rest_urlencoded POST ${url%/}/admin/mail/users/add "$admin_email" "$admin_pass" --insecure -- "email=$user_email" "password=${user_pass:-$pw}" 2>/dev/null
then
echo "Unable to add user $user_email: rc=$? err=$REST_ERROR" 1>&2
return 2
else
echo "Add: $user"
fi
done
return 0
}
populate_miab_aliases() {
local url="$1"
local admin_email="${2:-$EMAIL_ADDR}"
local admin_pass="${3:-$EMAIL_PW}"
shift; shift; shift # remaining arguments are aliases to add
# each "alias" argument is in the format "email-alias > forward-to"
[ $# -eq 0 ] && return 0
#
# get the existing aliases
#
local current_aliases=() alias
if ! rest_urlencoded GET ${url%/}/admin/mail/aliases "$admin_email" "$admin_pass" --insecure 2>/dev/null; then
echo "Unable to enumerate aliases: rc=$? err=$REST_ERROR" 1>&2
return 1
fi
for alias in $REST_OUTPUT; do
current_aliases+=("$alias")
done
#
# add the new aliases
#
local aliasdef
for aliasdef; do
alias="$(awk -F'[> ]' '{print $1}' <<<"$aliasdef")"
local forwards_to="$(sed 's/.*> *\(.*\)/\1/' <<<"$aliasdef")"
if array_contains "$alias" "${current_aliases[@]}"; then
echo "Not adding alias $aliasdef: already exists"
elif ! rest_urlencoded POST ${url%/}/admin/mail/aliases/add "$admin_email" "$admin_pass" --insecure -- "address=$alias" "forwards_to=$forwards_to" 2>/dev/null
then
echo "Unable to add alias $alias: rc=$? err=$REST_ERROR" 1>&2
return 2
else
echo "Add: $aliasdef"
fi
done
return 0
}

112
tests/lib/rest.sh Normal file
View File

@ -0,0 +1,112 @@
#
# REST helper functions
#
# requirements:
# system packages: [ curl ]
# lib scripts: [ system.sh, color-output.sh ]
#
rest_urlencoded() {
# Issue a REST call having data urlencoded
#
# eg: rest_urlencoded POST /admin/mail/users/add "email=alice@abc.com" "password=secret"
#
# When providing a URI (/mail/users/add) and not a URL
# (https://host/mail/users/add), PRIMARY_HOSTNAME must be set!
#
# The function will set the following global variables regardless
# of exit code:
# REST_HTTP_CODE
# REST_OUTPUT
# REST_ERROR
#
# Return values:
# 0 indicates success (curl returned 0 or a code deemed to be
# successful and HTTP status is >=200 but <300)
# 1 curl returned with non-zero code that indicates and error
# 2 the response status was <200 or >= 300
#
# Debug messages are sent to stderr
#
local verb="$1" # eg "POST"
local uri="$2" # eg "/mail/users/add"
local auth_user="$3"
local auth_pass="$4"
shift; shift; shift; shift # remaining arguments are data or curl args
local url
local is_local="false"
case "$uri" in
http:* | https:* )
url="$uri"
;;
* )
url="https://$PRIMARY_HOSTNAME${uri}"
is_local="true"
;;
esac
local data=()
local item output onlydata="false"
for item; do
case "$item" in
-- )
onlydata="true"
;;
--* )
# curl argument
if $onlydata; then
data+=("--data-urlencode" "$item");
else
data+=("$item")
fi
;;
* )
onlydata="true"
data+=("--data-urlencode" "$item");
;;
esac
done
echo "spawn: curl -w \"%{http_code}\" -X $verb --user \"${auth_user}:xxx\" ${data[@]} $url" 1>&2
output=$(curl -s -S -w "%{http_code}" -X $verb --user "${auth_user}:${auth_pass}" "${data[@]}" $url)
local code=$?
# http status is last 3 characters of output, extract it
REST_HTTP_CODE=$(awk '{S=substr($0,length($0)-2)} END {print S}' <<<"$output")
REST_OUTPUT=$(awk 'BEGIN{L=""}{ if(L!="") print L; L=$0 } END { print substr(L,1,length(L)-3) }' <<<"$output")
REST_ERROR=""
[ -z "$REST_HTTP_CODE" ] && REST_HTTP_CODE="000"
if [ $code -ne 0 ]; then
if [ $code -eq 56 -a $REST_HTTP_CODE -eq 200 ]; then
# this is okay, I guess. happens sometimes during
# POST /admin/mail/aliases/remove
# 56=Unexpected EOF
echo "Ignoring curl return code 56 due to 200 status" 1>&2
elif [ $code -ne 16 -o $REST_HTTP_CODE -ne 200 ]; then
# any error code will fail the rest call except for a 16
# with a 200 HTTP status.
# 16="a problem was detected in the HTTP2 framing layer. This is somewhat generic and can be one out of several problems"
REST_ERROR="CURL failed with code $code"
echo "${F_DANGER}$REST_ERROR${F_RESET}" 1>&2
echo "$output" 1>&2
return 1
fi
fi
if [ $REST_HTTP_CODE -lt 200 -o $REST_HTTP_CODE -ge 300 ]; then
REST_ERROR="REST status $REST_HTTP_CODE: $REST_OUTPUT"
echo "${F_DANGER}$REST_ERROR${F_RESET}" 1>&2
if $is_local && [ $REST_HTTP_CODE -ge 500 ]; then
echo -n "$F_WARN"
tail -100 /var/log/syslog
echo -n "$F_RESET"
fi
return 2
fi
echo "CURL succeded, HTTP status $REST_HTTP_CODE" 1>&2
echo "$output" 1>&2
return 0
}

102
tests/lib/system.sh Normal file
View File

@ -0,0 +1,102 @@
wait_for_apt() {
# check to see if other package managers have a lock on new
# installs, and wait for them to finish
#
# returns non-zero if waiting times out (currently ~600 seconds)
local count=0
while fuser /var/lib/dpkg/lock >/dev/null 2>&1 || fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do
sleep 6
let count+=1
if [ $count -eq 1 ]; then
echo -n "Waiting for other package manager to finish..."
elif [ $count -gt 100 ]; then
echo -n "FAILED"
return 1
else
echo -n "${count}.."
fi
done
[ $count -ge 1 ] && echo ""
}
dump_file() {
local log_file="$1"
local lines="$2"
local title="DUMP OF $log_file"
echo ""
echo "--------"
echo -n "-------- $log_file"
if [ ! -z "$lines" ]; then
echo " (last $line lines)"
else
echo ""
fi
echo "--------"
if [ ! -e "$log_file" ]; then
echo "DOES NOT EXIST"
elif [ ! -z "$lines" ]; then
tail -$lines "$log_file"
else
cat "$log_file"
fi
}
dump_file_if_exists() {
[ ! -e "$1" ] && return
dump_file "$@"
}
update_system_time() {
if [ ! -x /usr/sbin/ntpdate ]; then
wait_for_apt
apt-get install -y -qq ntpdate || return 1
fi
ntpdate -s ntp.ubuntu.com && echo "System time updated"
}
set_system_hostname() {
# set the system hostname to the FQDN specified or
# PRIMARY_HOSTNAME if no FQDN was given
local fqdn="${1:-$PRIMARY_HOSTNAME}"
local host="$(awk -F. '{print $1}' <<< "$fqdn")"
sed -i 's/^127\.0\.1\.1[ \t].*/127.0.1.1 '"$fqdn $host ip4-loopback/" /etc/hosts || return 1
#hostname "$host" || return 1
#echo "$host" > /etc/hostname
return 0
}
install_docker() {
if [ -x /usr/bin/docker ]; then
echo "Docker already installed"
return 0
fi
wait_for_apt
apt-get install -y -qq \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common \
|| return 1
wait_for_apt
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - \
|| return 2
wait_for_apt
apt-key fingerprint 0EBFCD88 || return 3
wait_for_apt
add-apt-repository -y --update "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" || return 4
wait_for_apt
apt-get install -y -qq \
docker-ce \
docker-ce-cli \
containerd.io \
|| return 5
}

View File

@ -11,7 +11,7 @@ cd "$(dirname $0)"
# load global functions and variables
. suites/_init.sh
runner_suites=(
default_suites=(
ldap-connection
ldap-access
mail-basic
@ -21,21 +21,39 @@ runner_suites=(
management-users
)
extra_suites=(
remote-nextcloud
"upgrade-<name>"
)
usage() {
echo ""
echo "Usage: $(basename $0) [-failfatal] [suite-name ...]"
echo "Valid suite names:"
for runner_suite in ${runner_suites[@]}; do
echo "Usage: $(basename $0) [options] [suite-name ...]"
echo "Run QA tests"
echo ""
echo "Default test suites:"
echo "--------------------"
for runner_suite in ${default_suites[@]}; do
echo " $runner_suite"
done
echo "If no suite-name(s) given, all suites are run"
echo ""
echo "Extra test suites:"
echo "------------------"
echo " remote-nextcloud : test the setup mod for remote Nextcloud"
echo " upgrade-<name> : verify an upgrade using named populate data"
echo ""
echo "If no suite-name(s) are given, all default suites are run"
echo ""
echo "Options:"
echo "--------"
echo " -failfatal The runner will stop if any test fails"
echo " -dumpoutput After all tests have run, dump all failed test output"
echo " -no-smtp-remote Skip tests requiring a remote SMTP server"
echo ""
echo "Output directory: $(dirname $0)/${base_outputdir}"
echo "Output directory: ${BASE_OUTPUTDIR}"
echo ""
exit 1
}
@ -59,7 +77,26 @@ while [ $# -gt 0 ]; do
;;
* )
# run named suite
if array_contains "$1" ${runner_suites[@]}; then
if [ $OVERALL_COUNT_SUITES -eq 0 ]; then
rm -rf "${BASE_OUTPUTDIR}"
fi
case "$1" in
default )
# run all default suites
for suite in ${default_suites[@]}; do
. suites/$suite.sh
done
;;
upgrade-* )
# run upgrade suite with named populate data
. "suites/upgrade.sh" "$(awk -F- '{print $2}' <<< "$1")"
;;
* )
if array_contains "$1" "${default_suites[@]}" || \
array_contains "$1" "${extra_suites[@]}"
then
# run specified suite
. "suites/$1.sh"
else
echo "Unknown suite '$1'" 1>&2
@ -67,14 +104,15 @@ while [ $# -gt 0 ]; do
fi
;;
esac
esac
shift
done
# if no suites specified on command line, run all suites
# if no suites specified on command line, run all default suites
if [ $OVERALL_COUNT_SUITES -eq 0 ]; then
rm -rf "${base_outputdir}"
for runner_suite in ${runner_suites[@]}; do
. suites/$runner_suite.sh
rm -rf "${BASE_OUTPUTDIR}"
for suite in ${default_suites[@]}; do
. suites/$suite.sh
done
fi

View File

@ -6,12 +6,15 @@
set +eu
# load test suite helper functions
. lib/all.sh "lib" || exit 1
. suites/_ldap-functions.sh || exit 1
. suites/_mail-functions.sh || exit 1
. suites/_mgmt-functions.sh || exit 1
# globals - all global variables are UPPERCASE
BASE_OUTPUTDIR="out"
ASSETS_DIR="assets"
MIAB_DIR=".."
BASE_OUTPUTDIR="$(realpath out)/$(hostname | awk -F. '{print $1}')"
PYMAIL="./test_mail.py"
declare -i OVERALL_SUCCESSES=0
declare -i OVERALL_FAILURES=0
@ -19,10 +22,6 @@ declare -i OVERALL_SKIPPED=0
declare -i OVERALL_COUNT=0
declare -i OVERALL_COUNT_SUITES=0
# ansi escapes for hilighting text
F_DANGER=$(echo -e "\033[31m")
F_WARN=$(echo -e "\033[93m")
F_RESET=$(echo -e "\033[39m")
# options
FAILURE_IS_FATAL=no
@ -45,11 +44,12 @@ suite_start() {
mkdir -p "$OUTDIR"
echo ""
echo "Starting suite: $SUITE_NAME"
suite_setup "$2"
shift
suite_setup "$@"
}
suite_end() {
suite_cleanup "$1"
suite_cleanup "$@"
echo "Suite $SUITE_NAME finished"
let OVERALL_SUCCESSES+=$SUITE_COUNT_SUCCESS
let OVERALL_FAILURES+=$SUITE_COUNT_FAILURE
@ -61,14 +61,16 @@ suite_end() {
suite_setup() {
[ -z "$1" ] && return 0
TEST_OF="$OUTDIR/setup"
eval "$1"
local script
for script; do eval "$script"; done
TEST_OF=""
}
suite_cleanup() {
[ -z "$1" ] && return 0
TEST_OF="$OUTDIR/cleanup"
eval "$1"
local script
for script; do eval "$script"; done
TEST_OF=""
}
@ -105,7 +107,7 @@ test_end() {
let idx+=1
done
echo "$TEST_OF" >>$FAILED_TESTS_MANIFEST
echo " see: $(dirname $0)/$TEST_OF"
echo " see: $TEST_OF"
let SUITE_COUNT_FAILURE+=1
if [ "$FAILURE_IS_FATAL" == "yes" ]; then
record "FATAL: failures are fatal option enabled"
@ -153,7 +155,12 @@ test_skip() {
}
skip_test() {
# return 0 if we should skip the current test
# call from within a test to check whether the test will be
# skipped
#
# returns 0 if the current test was skipped in which case your test
# function must immediately call 'test_end' and return
#
if [ "$SKIP_REMOTE_SMTP_TESTS" == "yes" ] &&
array_contains "remote-smtp" "$@";
then
@ -187,16 +194,6 @@ die() {
exit 1
}
array_contains() {
local searchfor="$1"
shift
local item
for item; do
[ "$item" == "$searchfor" ] && return 0
done
return 1
}
python_error() {
# finds tracebacks and outputs just the final error message of
# each
@ -205,6 +202,12 @@ python_error() {
[ $? -eq 1 ] && echo "$output"
}
copy_or_die() {
local src="$1"
local dst="$2"
cp "$src" "$dst" || die "Unable to copy '$src' => '$dst'"
}
dump_failed_tests_output() {
if [ "$DUMP_FAILED_TESTS_OUTPUT" == "yes" ]; then
echo ""

View File

@ -1,11 +1,10 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
generate_uuid() {
local uuid
uuid=$(python3 -c "import uuid; print(uuid.uuid4())")
[ $? -ne 0 ] && die "Unable to generate a uuid"
echo "$uuid"
}
# requirements:
# system packages: [ ldap-utils ]
# setup scripts: [ functions-ldap.sh ]
# setup artifacts: [ miab_ldap.conf ]
delete_user() {
local email="$1"
@ -32,12 +31,13 @@ create_user() {
local priv="${3:-test}"
local localpart="$(awk -F@ '{print $1}' <<< "$email")"
local domainpart="$(awk -F@ '{print $2}' <<< "$email")"
local uid="$localpart"
#local uid="$localpart"
local uid="$(sha1 "$email")"
local dn="uid=${uid},${LDAP_USERS_BASE}"
delete_user "$email"
record "[create user $email]"
record "[create user $email ($dn)]"
delete_dn "$dn"
ldapadd -H "$LDAP_URL" -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >>$TEST_OF 2>&1 <<EOF

View File

@ -10,14 +10,14 @@ ensure_root_user() {
# ensure there is a local email account for root.
#
# on exit, ROOT, ROOT_MAILDROP, and ROOT_DN are set, and if no
# account exists, a new root@$(hostname) is created having a
# account exists, a new root@$(hostname --fqdn) is created having a
# random password
#
if [ ! -z "$ROOT_MAILDROP" ]; then
# already have it
return
fi
ROOT="${USER}@$(hostname)"
ROOT="${USER}@$(hostname --fqdn || hostname)"
record "[Find user $ROOT]"
get_attribute "$LDAP_USERS_BASE" "mail=$ROOT" "maildrop"
ROOT_MAILDROP="$ATTR_VALUE"

View File

@ -44,49 +44,9 @@ mgmt_rest() {
local uri="$2" # eg "/mail/users/add"
shift; shift; # remaining arguments are data
local auth_user="${MGMT_ADMIN_EMAIL}"
local auth_pass="${MGMT_ADMIN_PW}"
local url="https://$PRIMARY_HOSTNAME${uri}"
local data=()
local item output
for item; do data+=("--data-urlencode" "$item"); done
record "spawn: curl -w \"%{http_code}\" -X $verb --user \"${auth_user}:xxx\" ${data[@]} $url"
output=$(curl -s -S -w "%{http_code}" -X $verb --user "${auth_user}:${auth_pass}" "${data[@]}" $url 2>>$TEST_OF)
local code=$?
# http status is last 3 characters of output, extract it
REST_HTTP_CODE=$(awk '{S=substr($0,length($0)-2)} END {print S}' <<<"$output")
REST_OUTPUT=$(awk 'BEGIN{L=""}{ if(L!="") print L; L=$0 } END { print substr(L,1,length(L)-3) }' <<<"$output")
REST_ERROR=""
[ -z "$REST_HTTP_CODE" ] && REST_HTTP_CODE="000"
if [ $code -ne 0 ]; then
if [ $code -eq 56 -a $REST_HTTP_CODE -eq 200 ]; then
# this is okay, I guess. happens sometimes during
# POST /admin/mail/aliases/remove
# 56=Unexpected EOF
record "Ignoring curl return code 56 due to 200 status"
elif [ $code -ne 16 -o $REST_HTTP_CODE -ne 200 ]; then
# any error code will fail the rest call except for a 16
# with a 200 HTTP status.
# 16="a problem was detected in the HTTP2 framing layer. This is somewhat generic and can be one out of several problems"
REST_ERROR="CURL failed with code $code"
record "${F_DANGER}$REST_ERROR${F_RESET}"
record "$output"
return 1
fi
fi
if [ $REST_HTTP_CODE -lt 200 -o $REST_HTTP_CODE -ge 300 ]; then
REST_ERROR="REST status $REST_HTTP_CODE: $REST_OUTPUT"
record "${F_DANGER}$REST_ERROR${F_RESET}"
return 2
fi
record "CURL succeded, HTTP status $REST_HTTP_CODE"
record "$output"
return 0
# call function from lib/rest.sh
rest_urlencoded "$verb" "$uri" "${MGMT_ADMIN_EMAIL}" "${MGMT_ADMIN_PW}" "$@" >>$TEST_OF 2>&1
return $?
}

View File

@ -189,7 +189,7 @@ EOF
}
suite_start "mail-access"
suite_start "mail-access" ensure_root_user
test_greylisting
test_relay_prohibited

View File

@ -67,7 +67,7 @@ test_self_send_receive() {
suite_start "mail-basic"
suite_start "mail-basic" ensure_root_user
test_trial_send_local
test_trial_send_remote

View File

@ -0,0 +1,144 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
#
# Test the setup modification script setup/mods.available/remote-nextcloud.sh
# Prerequisites:
#
# - Nextcloud is already installed and MiaB-LDAP is already
# configured to use it.
#
# ie. remote-nextcloud.sh was run on MiaB-LDAP by
# setup/start.sh because there was a symbolic link from
# local/remote-nextcloud.sh to the script in
# mods.available
#
# - The remote Nextcloud has been configured to use MiaB-LDAP
# for users and groups.
#
# ie. remote-nextcloud-use-miab.sh was copied to the remote Nextcloud
# server and was run successfully there
#
is_configured() {
. /etc/mailinabox_mods.conf
if [ $? -ne 0 -o -z "$NC_HOST" ]; then
return 1
fi
return 0
}
assert_is_configured() {
if ! is_configured; then
test_failure "remote-nextcloud is not configured"
return 1
fi
return 0
}
assert_roundcube_carddav_contact_exists() {
local user="$1"
local pass="$2"
local c_uid="$3"
local output
record "[checking that roundcube contact with vcard UID=$c_uid exists]"
roundcube_carddav_contact_exists "$user" "$pass" "$c_uid" 2>>$TEST_OF
local rc=$?
if [ $rc -eq 0 ]; then
return
elif [ $rc -eq 1 ]; then
test_failure "Contact not found in Roundcube"
record "Not found"
record "Existing entries:"
roundcube_dump_contacts >>$TEST_OF 2>&1
else
test_failure "Error querying roundcube contacts"
return
fi
}
test_mail_from_nextcloud() {
test_start "mail_from_nextcloud"
test_end
}
test_nextcloud_contacts() {
test_start "nextcloud-contacts"
if ! assert_is_configured; then
test_end
return
fi
local alice="alice.nc@somedomain.com"
local alice_pw="$(generate_password 16)"
# create local user alice
mgmt_assert_create_user "$alice" "$alice_pw"
#
# 1. create contact in Nextcloud - ensure it is available in Roundcube
#
# this will validate Nextcloud's ability to authenticate users via
# LDAP and that Roundcube is able to reach Nextcloud for contacts
#
#record "[create address book 'contacts' for $alice]"
#carddav_make_addressbook "$alice" "$alice_pw" "contacts" 2>>$TEST_OF
# add new contact to alice's Nextcloud account using CardDAV API
local c_uid="$(generate_uuid)"
record "[add contact 'JimIno' to $alice]"
if ! carddav_add_contact \
"$alice" \
"$alice_pw" \
"JimIno" \
"555-1212" \
"jim@ino.com" \
"$c_uid" \
2>>$TEST_OF
then
test_failure "Could not add contact for $alice in Nextcloud: $REST_ERROR_BRIEF"
test_end
return
fi
# force a refresh/sync of the contacts in Roundcube
record "[forcing refresh of roundcube contact for $alice]"
roundcube_force_carddav_refresh "$alice" "$alice_pw" >>$TEST_OF 2>&1 || \
test_failure "Could not refresh roundcube contacts for $alice"
# query the roundcube sqlite database for the new contact
assert_roundcube_carddav_contact_exists "$alice" "$alice_pw" "$c_uid"
# delete the contact
record "[delete contact with vcard uid '$c_uid' from $alice]"
carddav_delete_contact "$alice" "$alice_pw" "$c_uid" 2>>$TEST_OF || \
test_failure "Unable to delete contact for $alice in Nextcloud"
#
# 2. create contact in Roundcube - ensure contact appears in Nextcloud
#
# TODO
# clean up
mgmt_assert_delete_user "$alice"
test_end
}
suite_start "remote-nextcloud" mgmt_start
#test_mail_from_nextcloud
test_nextcloud_contacts
suite_end mgmt_end

48
tests/suites/upgrade.sh Normal file
View File

@ -0,0 +1,48 @@
#
# the system must have been populated proir to any upgrade with one of
# the tests/system-setup/populate scripts to use this suite
#
# supply the name of the populate script that was used as an argument
# eg. if basic-populate.sh was used to populate, supply "basic" to the
# script as an argument
#
verify_populate() {
local populate_name="$1"
local verify_script="system-setup/populate/${populate_name}-verify.sh"
test_start "verify '$populate_name' population set"
if [ ! -e "$verify_script" ]; then
test_failure "Verify script $(basename "$verify_script") does not exist"
else
record "[run verify-upgrade script $verify_script]"
local output rc
output=$("$verify_script" 2>>$TEST_OF)
rc=$?
if [ $rc -ne 0 ]
then
if [ $rc -eq 127 ]; then
test_failure "verify script would not run (wd=$(pwd))"
else
test_failure "verify script exited with $rc: $output"
fi
fi
fi
test_end
}
suite_start "upgrade"
export ASSETS_DIR
export MIAB_DIR
verify_populate "$1"
suite_end

View File

@ -0,0 +1,31 @@
This directory contains scripts used to populate a MiaB installation
with known values, and then subsequently verify that MiaB continues to
operate poperly after an upgrade or setup mod change.
Each "named" populate set of scripts should contain at least two
shell scripts:
1. <name>-populate.sh : populates the installation
2. <name>-verify.sh : verifies operation after upgrade
The system-setup/* scripts run the populate script, and the test
runner's 'upgrade' test suite runs the verify script.
These scripts are run, not sourced.
Expected script output and return value:
1. All debug output must go to stderr
2. Result messages must be sent to stdout (a single line, preferrably)
3. Return 0 if successfully passed verification
4. Return non-zero if failed verification
The working directory for <name>-populate.sh is the Mail-in-a-Box root
directory.
The working directory for <name>-verify.sh is 'tests' (because the
test runner always changes the working directory there to limit
contamination of the source tree). Use MIAB_DIR and ASSETS_DIR, if
needed.

View File

@ -0,0 +1,10 @@
#
# requires:
# lib scripts: [ misc.sh ]
# system-setup scripts: [ setup-defaults.sh ]
#
TEST_USER="anna@$(email_domainpart "$EMAIL_ADDR")"
TEST_USER_PASS="$(static_qa_password)"
TEST_USER_CONTACT_UUID="e0642b47-9104-4adb-adfd-5f907d04216a"
TEST_USER_CONTACT_EMAIL="sam@bff.org"

View File

@ -0,0 +1,48 @@
#!/bin/bash
. "$(dirname "$0")/../setup-defaults.sh" || exit 1
. "$(dirname "$0")/../../lib/all.sh" "$(dirname "$0")/../../lib" || exit 1
. "$(dirname "$0")/basic-data.sh" || exit 1
#
# Add user
#
if ! populate_miab_users "" "" "" "${TEST_USER}:${TEST_USER_PASS}"
then
echo "Unable to add user"
exit 1
fi
#
# Add Nextcloud contact and force Roundcube contact sync to ensure the
# roundcube carddav addressbooks and contacts tables are populated in
# case a remote nextcloud is subsequently configured and the
# syncronization disabled.
#
if ! carddav_ls "$TEST_USER" "$TEST_USER_PASS" --insecure 2>/dev/null
then
echo "Could not enumerate contacts: $REST_ERROR"
exit 1
fi
echo "Current contacts count: ${#FILES[@]}"
if array_contains "$TEST_USER_CONTACT_UUID.vcf" "${FILES[@]}"; then
echo "Contact $TEST_USER_CONTACT_UUID already present"
else
if ! carddav_add_contact "$TEST_USER" "$TEST_USER_PASS" "Anna" "666-1111" "$TEST_USER_CONTACT_EMAIL" "$TEST_USER_CONTACT_UUID" --insecure 2>/dev/null
then
echo "Could not add contact: $REST_ERROR"
exit 1
fi
echo "Force Roundcube contact sync"
if ! roundcube_force_carddav_refresh "$TEST_USER" "$TEST_USER_PASS"
then
echo "Roundcube <-> Nextcloud contact sync failed"
exit 1
fi
fi
exit 0

View File

@ -0,0 +1,53 @@
#!/bin/bash
. "$(dirname "$0")/../setup-defaults.sh" || exit 1
. "$(dirname "$0")/../../lib/all.sh" "$(dirname "$0")/../../lib" || exit 1
. "$(dirname "$0")/basic-data.sh" || exit 1
. /etc/mailinabox.conf || exit 1
# 1. the test user can still log in and send mail
echo "[User can still log in with their old passwords and send mail]" 1>&2
echo "python3 test_mail.py $PRIVATE_IP $TEST_USER $TEST_USER_PASS" 1>&2
python3 test_mail.py "$PRIVATE_IP" "$TEST_USER" "$TEST_USER_PASS" 1>&2
if [ $? -ne 0 ]; then
echo "Basic mail functionality test failed"
exit 1
fi
# 2. the test user's contact is still accessible in Roundcube
echo "[Force Roundcube contact sync]" 1>&2
# if MiaB's Nextcloud carddav configuration was removed all the
# contacts for it will be removed in the Roundcube database after the
# sync
if ! roundcube_force_carddav_refresh "$TEST_USER" "$TEST_USER_PASS" 1>&2
then
echo "Roundcube <-> Nextcloud contact sync failed ($?)"
exit 1
fi
echo "[Ensure old Nextcloud contacts are still present]" 1>&2
echo "sqlite3 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite \"select email from carddav_contacts where cuid='$TEST_USER_CONTACT_UUID'\"" 1>&2
output=$(sqlite3 "$STORAGE_ROOT/mail/roundcube/roundcube.sqlite" "select email from carddav_contacts where cuid='$TEST_USER_CONTACT_UUID'")
rc=$?
if [ $rc -ne 0 ]
then
echo "Querying Roundcube's sqlite database failed ($rc)"
exit 1
else
echo "Success, found $output" 1>&2
fi
if [ "$output" != "$TEST_USER_CONTACT_EMAIL" ]
then
echo "Unexpected email for contact uuid: got '$output', expected '$TEST_USER_CONTACT_EMAIL'"
exit 1
fi
echo "OK basic-verify passed"
exit 0

View File

@ -0,0 +1,223 @@
#!/bin/bash
# setup MiaB-LDAP with a remote Nextcloud running on the same
# host under Docker exposed as localhost:8000
#
# to use:
# on a fresh Ubuntu:
# 1. checkout or copy the MiaB-LDAP code to ~/mailinabox
# 2. cd ~/mailinabox
# 3. sudo tests/system-setup/remote-nextcloud-docker.sh
#
# when complete you should have a working MiaB-LDAP and Nextcloud
#
# You can access MiaB-LDAP using your browser to the Ubuntu system in
# the normal way, (eg: https://<ubuntu-box>/admin).
#
# Nextcloud is running under Docker on the ubuntu box, so to access it
# you'll first need to ssh into the ubuntu box with port-forrwarding
# enabled.
#
# eg: ssh -L 8000:localhost:8000 user@<ubuntu-box>
#
# Then, in your browser visit http://localhost:8000/.
#
# See setup-defaults.sh for usernames and passwords.
#
# ensure working directory
if [ ! -d "tests/system-setup" ]; then
echo "This script must be run from the MiaB root directory"
exit 1
fi
# load helper scripts
. "tests/lib/all.sh" "tests/lib" || die "Could not load lib scripts"
. "tests/system-setup/setup-defaults.sh" || die "Could not load setup-defaults"
. "tests/system-setup/setup-funcs.sh" || die "Could not load setup-funcs"
# ensure running as root
if [ "$EUID" != "0" ]; then
die "This script must be run as root (sudo)"
fi
init() {
H1 "INIT"
init_test_system
init_miab_testing || die "Initialization failed"
}
install_nextcloud_docker() {
H1 "INSTALL NEXTCLOUD ON DOCKER"
# install Docker
H2 "Install Docker"
install_docker || die "Could not install Docker! ($?)"
# run Nextcloud docker image
H2 "Start Nextcloud docker container"
local container_started="true"
if [ -z "$(docker ps -f NAME=NC -q)" ]; then
docker run -d --name NC -p 8000:80 \
--add-host "$PRIMARY_HOSTNAME:$PRIVATE_IP" \
--env SQLITE_DATABASE=nextclouddb.sqlite \
--env NEXTCLOUD_ADMIN_USER="$NC_ADMIN_USER" \
--env NEXTCLOUD_ADMIN_PASSWORD="$NC_ADMIN_PASSWORD" \
--env NEXTCLOUD_TRUSTED_DOMAINS="127.0.0.1 ::1" \
--env NEXTCLOUD_UPDATE=1 \
--env SMTP_HOST="$PRIMARY_HOSTNAME" \
--env SMTP_SECURE="tls" \
--env SMTP_PORT=587 \
--env SMTP_AUTHTYPE="LOGIN" \
--env SMTP_NAME="$EMAIL_ADDR" \
--env SMTP_PASSWORD="$EMAIL_PW" \
--env SMTP_FROM_ADDRESS="$(email_localpart "$EMAIL_ADDR")" \
--env MAIL_DOMAIN="$(email_domainpart "$EMAIL_ADDR")" \
nextcloud:latest \
|| die "Docker run failed!"
else
echo "Container already running"
container_started="false"
fi
# apt-get update
H2 "docker: apt-get update"
docker exec NC apt-get update || die "docker: apt-get update failed"
# allow LDAP access from docker image
H2 "Allow ldaps through firewall so Nextcloud can perform LDAP searches"
ufw allow ldaps || die "Unable to modify firewall to permit ldaps"
# add MiaB-LDAP's ca_certificate.pem to docker's trusted cert list
# (because setup/ssl.sh created its own self-signed ca)
H2 "docker: update trusted CA list"
docker cp \
$STORAGE_ROOT/ssl/ca_certificate.pem \
NC:/usr/local/share/ca-certificates/mailinabox.crt \
|| die "docker: copy ca_certificate.pem failed"
docker exec NC update-ca-certificates \
|| die "docker: update-ca-certificates failed"
# wait for Nextcloud installation to complete
H2 "Wait for Nextcloud installation to complete"
wait_for_docker_nextcloud NC installed || die "Giving up"
# install and enable Nextcloud apps
H2 "docker: install Nextcloud calendar app"
if ! docker exec -u www-data NC ./occ app:install calendar
then
$container_started || die "docker: installing calendar app failed"
fi
H2 "docker: install Nextcloud contacts app"
if ! docker exec -u www-data NC ./occ app:install contacts
then
$container_started || die "docker: installing contacts app failed"
fi
H2 "docker: enable user_ldap"
docker exec -u www-data NC ./occ app:enable user_ldap \
|| die "docker: enabling user_ldap failed ($?)"
# integrate Nextcloud with MiaB-LDAP
H2 "docker: integrate Nextcloud with MiaB-LDAP"
docker cp setup/mods.available/remote-nextcloud-use-miab.sh NC:/tmp \
|| die "docker: cp remote-nextcloud-use-miab.sh failed"
docker exec NC /tmp/remote-nextcloud-use-miab.sh \
. \
"$NC_ADMIN_USER" \
"$NC_ADMIN_PASSWORD" \
"$PRIMARY_HOSTNAME" \
"$LDAP_NEXTCLOUD_PASSWORD" \
|| die "docker: error running remote-nextcloud-use-miab.sh"
}
do_upgrade() {
local populate_name="$1"
if [ -e "$LOCAL_MODS_DIR/remote-nextcloud.sh" ]; then
# we install w/o remote nextcloud first so we can add
# a user w/contacts and ensure the contact exists in the
# new system
if [ ! -L "$LOCAL_MODS_DIR/remote-nextcloud.sh" ]; then
echo "Warning: $LOCAL_MODS_DIR/remote-nextcloud.sh is a regular file - should be a symlink"
fi
die "Error: $LOCAL_MODS_DIR/remote-nextcloud.sh exists - delete it and try again"
fi
# initialize test system
init
# install w/o remote Nextcloud
miab_ldap_install
# populate some data
[ ! -z "$populate_name" ] && populate_by_name "$populate_name"
# install Nextcloud in a Docker container (MiaB must be available)
install_nextcloud_docker
H1 "Enable remote-nextcloud mod"
enable_miab_mod "remote-nextcloud" \
|| die "Could not enable remote-nextcloud mod"
# re-run setup to use the remote Nextcloud
miab_ldap_install
}
do_default() {
# initialize test system
init
H1 "Enable remote-nextcloud mod"
enable_miab_mod "remote-nextcloud" \
|| die "Could not enable remote-nextcloud mod"
# run setup to use the remote Nextcloud (doesn't need to be available)
miab_ldap_install
# install Nextcloud in a Docker container (MiaB must be available)
install_nextcloud_docker
}
case "$1" in
upgrade )
# Runs this sequence:
# 1. setup w/o remote nextcloud
# 2. if an additional argument is given, populate the MiaB
# installation
# 3. install a remote nextcloud
# 4. enable remote-nextcloud mod
# 5. re-run setup
#
shift
do_upgrade "$@"
;;
"" | default )
# Runs this sequence:
# 1. setup w/remote nextcloud
# 2. install and connect the remote nextcloud
do_default
;;
* )
echo "Unknown option $1"
exit 1
;;
esac

View File

@ -0,0 +1,36 @@
#!/bin/bash
# Used by setup/start.sh
export NONINTERACTIVE=${NONINTERACTIVE:-1}
export SKIP_NETWORK_CHECKS=${SKIP_NETWORK_CHECKS:-1}
export STORAGE_USER="${STORAGE_USER:-user-data}"
export STORAGE_ROOT="${STORAGE_ROOT:-/home/$STORAGE_USER}"
export EMAIL_ADDR="${EMAIL_ADDR:-qa@abc.com}"
export EMAIL_PW="${EMAIL_PW:-Test_1234}"
export PUBLIC_IP="${PUBLIC_IP:-$(source ${MIAB_DIR:-.}/setup/functions.sh; get_default_privateip 4)}"
export LOCAL_MODS_DIR="${LOCAL_MODS_DIR:-local}"
if [ "$TRAVIS" == "true" ]; then
export PRIMARY_HOSTNAME=${PRIMARY_HOSTNAME:-box.abc.com}
elif [ -z "$PRIMARY_HOSTNAME" ]; then
export PRIMARY_HOSTNAME=${PRIMARY_HOSTNAME:-$(hostname --fqdn || hostname)}
fi
# Placing this var in STORAGE_ROOT/ldap/miab_ldap.conf before running
# setup/start.sh will avoid a random password from being used for the
# Nextcloud LDAP service account
export LDAP_NEXTCLOUD_PASSWORD=${LDAP_NEXTCLOUD_PASSWORD:-Test_LDAP_1234}
# Used by setup/mods.available/remote-nextcloud.sh. These define to
# MiaB-LDAP the remote Nextcloud that serves calendar and contacts
export NC_PROTO=${NC_PROTO:-http}
export NC_HOST=${NC_HOST:-127.0.0.1}
export NC_PORT=${NC_PORT:-8000}
export NC_PREFIX=${NC_PREFIX:-/}
# For setup scripts that may be installing a remote Nextcloud
export NC_ADMIN_USER="${NC_ADMIN_USER:-admin}"
export NC_ADMIN_PASSWORD="${NC_ADMIN_PASSWORD:-Test_1234}"
# For setup scripts that install upstream versions
export MIAB_UPSTREAM_GIT="https://github.com/mail-in-a-box/mailinabox.git"

187
tests/system-setup/setup-funcs.sh Executable file
View File

@ -0,0 +1,187 @@
#
# requires:
#
# test scripts: [ lib/misc.sh, lib/system.sh ]
#
die() {
local msg="$1"
echo "$msg" 1>&2
exit 1
}
wait_for_docker_nextcloud() {
local container="$1"
local config_key="$2"
echo -n "Waiting ..."
local count=0
while true; do
if [ $count -ge 10 ]; then
echo "FAILED"
return 1
fi
sleep 6
let count+=1
if [ $(docker exec "$container" php -n -r "include 'config/config.php'; print \$CONFIG['$config_key']?'true':'false';") == "true" ]; then
echo "ok"
break
fi
echo -n "${count}..."
done
return 0
}
dump_conf_files() {
local skip
if [ $# -eq 0 ]; then
skip="false"
else
skip="true"
for item; do
if is_true "$item"; then
skip="false"
break
fi
done
fi
if [ "$skip" == "false" ]; then
dump_file "/etc/mailinabox.conf"
dump_file_if_exists "/etc/mailinabox_mods.conf"
dump_file "/etc/hosts"
dump_file "/etc/nsswitch.conf"
dump_file "/etc/resolv.conf"
dump_file "/etc/nsd/nsd.conf"
#dump_file "/etc/postfix/main.cf"
fi
}
#
# Initialize the test system
# hostname, time, apt update/upgrade, etc
#
# Errors are fatal
#
init_test_system() {
H2 "Update /etc/hosts"
set_system_hostname || die "Could not set hostname"
# update system time
H2 "Set system time"
update_system_time || echo "Ignoring error..."
# update package lists before installing anything
H2 "apt-get update"
wait_for_apt
apt-get update -qq || die "apt-get update failed!"
# upgrade packages - if we don't do this and something like bind
# is upgraded through automatic upgrades (because maybe MiaB was
# previously installed), it may cause problems with the rest of
# the setup, such as with name resolution failures
if is_false "$TRAVIS"; then
H2 "apt-get upgrade"
wait_for_apt
apt-get upgrade -qq || die "apt-get upgrade failed!"
fi
}
#
# Initialize the test system with QA prerequisites
# Anything needed to use the test runner, speed up the installation,
# etc
#
init_miab_testing() {
[ -z "$STORAGE_ROOT" ] \
&& echo "Error: STORAGE_ROOT not set" 1>&2 \
&& return 1
H2 "QA prerequisites"
local rc=0
# python3-dnspython: is used by the python scripts in 'tests' and is
# not installed by setup
wait_for_apt
apt-get install -y -qq python3-dnspython
# copy in pre-built MiaB-LDAP ssl files
# 1. avoid the lengthy generation of DH params
mkdir -p $STORAGE_ROOT/ssl \
|| (echo "Unable to create $STORAGE_ROOT/ssl ($?)" && rc=1)
cp tests/assets/ssl/dh2048.pem $STORAGE_ROOT/ssl \
|| (echo "Copy dhparams failed ($?)" && rc=1)
# create miab_ldap.conf to specify what the Nextcloud LDAP service
# account password will be to avoid a random one created by start.sh
if [ ! -z "$LDAP_NEXTCLOUD_PASSWORD" ]; then
mkdir -p $STORAGE_ROOT/ldap \
|| (echo "Could not create $STORAGE_ROOT/ldap" && rc=1)
[ -e $STORAGE_ROOT/ldap/miab_ldap.conf ] && \
echo "Warning: exists: $STORAGE_ROOT/ldap/miab_ldap.conf" 1>&2
touch $STORAGE_ROOT/ldap/miab_ldap.conf || rc=1
if ! grep "^LDAP_NEXTCLOUD_PASSWORD=" $STORAGE_ROOT/ldap/miab_ldap.conf >/dev/null; then
echo "LDAP_NEXTCLOUD_PASSWORD=\"$LDAP_NEXTCLOUD_PASSWORD\"" >> $STORAGE_ROOT/ldap/miab_ldap.conf
fi
fi
return $rc
}
enable_miab_mod() {
local name="${1}.sh"
if [ ! -e "$LOCAL_MODS_DIR/$name" ]; then
mkdir -p "$LOCAL_MODS_DIR"
if ! ln -s "$(pwd)/setup/mods.available/$name" "$LOCAL_MODS_DIR/$name"
then
echo "Warning: copying instead of symlinking $LOCAL_MODS_DIR/$name"
cp "setup/mods.available/$name" "$LOCAL_MODS_DIR/$name"
fi
fi
}
tag_from_readme() {
# extract the recommended TAG from README.md
# sets a global "TAG"
local readme="${1:-README.md}"
TAG="$(grep -F 'git checkout' "$readme" | sed 's/.*\(v[0123456789]*\.[0123456789]*\).*/\1/')"
[ $? -ne 0 -o -z "$TAG" ] && return 1
return 0
}
miab_ldap_install() {
H1 "MIAB-LDAP INSTALL"
# ensure we're in a MiaB-LDAP working directory
if [ ! -e setup/ldap.sh ]; then
die "Cannot install: the working directory is not MiaB-LDAP!"
fi
if ! setup/start.sh; then
H1 "OUTPUT OF SELECT FILES"
dump_file "/var/log/syslog" 100
dump_conf_files "$TRAVIS"
H2; H2 "End"; H2
die "MiaB-LDAP setup/start.sh failed!"
fi
# set actual STORAGE_ROOT, STORAGE_USER, PRIVATE_IP, etc
. /etc/mailinabox.conf || die "Could not source /etc/mailinabox.conf"
}
populate_by_name() {
local populate_name="$1"
H1 "Populate Mail-in-a-Box ($populate_name)"
local populate_script="tests/system-setup/populate/${populate_name}-populate.sh"
if [ ! -e "$populate_script" ]; then
die "Does not exist: $populate_script"
fi
"$populate_script" || die "Failed: $populate_script"
}

View File

@ -0,0 +1,179 @@
#!/bin/bash
# setup MiaB-LDAP by:
# 1. installing upstream MiaB
# 2. adding some data (users/aliases/etc)
# 3. upgrading to MiaB-LDAP
#
# See setup-defaults.sh for usernames and passwords.
#
usage() {
echo "Usage: $(basename "$0")"
echo "Install MiaB-LDAP after installing upstream MiaB"
exit 1
}
# ensure working directory
if [ ! -d "tests/system-setup" ]; then
echo "This script must be run from the MiaB root directory"
exit 1
fi
# load helper scripts
. "tests/lib/all.sh" "tests/lib" || die "Could not load lib scripts"
. "tests/system-setup/setup-defaults.sh" || die "Could not load setup-defaults"
. "tests/system-setup/setup-funcs.sh" || die "Could not load setup-funcs"
# ensure running as root
if [ "$EUID" != "0" ]; then
die "This script must be run as root (sudo)"
fi
init() {
H1 "INIT"
init_test_system
init_miab_testing || die "Initialization failed"
}
upstream_install() {
local upstream_dir="$HOME/mailinabox-upstream"
H1 "INSTALL UPSTREAM"
[ ! -x /usr/bin/git ] && apt-get install -y -qq git
if [ ! -d "$upstream_dir" ] || [ -z "$(ls -A "$upstream_dir")" ] ; then
H2 "Cloning $MIAB_UPSTREAM_GIT"
rm -rf "$upstream_dir"
git clone "$MIAB_UPSTREAM_GIT" "$upstream_dir"
if [ $? -ne 0 ]; then
rm -rf "$upstream_dir"
die "git clone upstream failed!"
fi
if [ -z "$UPSTREAM_TAG" ]; then
tag_from_readme "$upstream_dir/README.md"
if [ $? -ne 0 ]; then
rm -rf "$upstream_dir"
die "Failed to extract TAG from $upstream_dir/README.md"
fi
fi
fi
pushd "$upstream_dir" >/dev/null
if [ ! -z "$UPSTREAM_TAG" ]; then
H2 "Checkout $UPSTREAM_TAG"
git checkout "$UPSTREAM_TAG" || die "git checkout $UPSTREAM_TAG failed"
fi
if [ "$TRAVIS" == "true" ]; then
# Apply a patch to setup/dns.sh so nsd will start. We must do
# it in the script and not after setup.sh runs because part of
# setup includes adding a new user via the management
# interface and that's where the management daemon crashes:
#
# "subprocess.CalledProcessError: Command '['/usr/sbin/service', 'nsd', 'restart']' returned non-zero exit status 1"
#
H2 "Patching upstream setup/dns.sh for Travis-CI"
sed -i 's|\(.*include:.*zones\.conf.*\)|cat >> /etc/nsd/nsd.conf <<EOF\n do-ip4: yes\n do-ip6: no\nremote-control:\n control-enable: no\nEOF\n\n\1|' setup/dns.sh \
|| die "Couldn't patch setup/dns.sh !!"
fi
H2 "Run upstream setup"
if ! setup/start.sh; then
echo "$F_WARN"
dump_file /var/log/syslog 100
echo "$F_RESET"
die "Upstream setup failed!"
fi
popd >/dev/null
H2 "Upstream info"
echo "Code version: $(git describe)"
echo "Migration version: $(cat "$STORAGE_ROOT/mailinabox.version")"
}
populate() {
local pw="$(static_qa_password)"
H1 "Add some Mail-in-a-Box data"
local users=()
users+=("betsy@$(email_domainpart "$EMAIL_ADDR"):$pw")
local alises=()
aliases+=("goalias@testdom.com > $(awk -F: {'print $1'} <<<"${users[0]}")")
aliases+=("nested@testdom.com > goalias@testdom.com")
H2 "Add users"
if ! populate_miab_users "" "" "" "${users[@]}"
then
die "Unable to add users"
fi
H2 "Add aliases"
if ! populate_miab_aliases "" "" "" "${aliases[@]}"
then
die "Unable to add aliases"
fi
}
# these are for debugging/testing
case "$1" in
capture )
. /etc/mailinabox.conf
installed_state_capture "/tmp/state/miab-ldap"
exit $?
;;
compare )
. /etc/mailinabox.conf
installed_state_compare "/tmp/state/upstream" "/tmp/state/miab-ldap"
exit $?
;;
populate )
. /etc/mailinabox.conf
populate_by_name "${1:-basic}"
exit $?
;;
esac
# install basic stuff, set the hostname, time, etc
init
# if MiaB-LDAP is already migrated, do not run upstream setup
[ -e /etc/mailinabox.conf ] && . /etc/mailinabox.conf
if [ -e "$STORAGE_ROOT/mailinabox.version" ] &&
[ $(cat "$STORAGE_ROOT/mailinabox.version") -ge 13 ]
then
echo "Warning: MiaB-LDAP is already installed! Skipping installation of upstream"
else
# install upstream
upstream_install
. /etc/mailinabox.conf
# populate some data
populate_by_name "${1:-basic}"
# capture upstream state
installed_state_capture "/tmp/state/upstream"
fi
# install miab-ldap and capture state
miab_ldap_install
installed_state_capture "/tmp/state/miab-ldap"
# compare states
if ! installed_state_compare "/tmp/state/upstream" "/tmp/state/miab-ldap"; then
die "Upstream and upgraded states are different !"
fi
#
# actual verification that mail sends/receives properly is done via
# the test runner ...
#

3
tests/vagrant/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.vagrant
out
*-console.log

42
tests/vagrant/Vagrantfile vendored Normal file
View File

@ -0,0 +1,42 @@
Vagrant.configure("2") do |config|
config.vm.synced_folder "../..", "/mailinabox", id: "mailinabox", automount: false
config.vm.provision "file", source:"globals.sh", destination:"globals.sh"
# remote-nextcloud-docker
config.vm.define "remote-nextcloud-docker" do |m1|
m1.vm.box = "ubuntu/bionic64"
m1.vm.provision :shell, :inline => <<-SH
source globals.sh || exit 1
export PRIMARY_HOSTNAME=qa1.abc.com
export FEATURE_MUNIN=false
cd /mailinabox
if tests/system-setup/remote-nextcloud-docker.sh upgrade basic
then
tests/runner.sh default remote-nextcloud upgrade-basic
fi
echo "EXITCODE: $?"
SH
end
# upgrade-from-upstream
config.vm.define "upgrade-from-upstream" do |m2|
m2.vm.box = "ubuntu/bionic64"
m2.vm.provision :shell, :inline => <<-SH
source globals.sh || exit 1
export PRIMARY_HOSTNAME=qa2.abc.com
export UPSTREAM_TAG=master
cd /mailinabox
if tests/system-setup/upgrade-from-upstream.sh basic
then
tests/runner.sh default upgrade-basic
fi
echo "EXITCODE: $?"
SH
end
end

2
tests/vagrant/globals.sh Normal file
View File

@ -0,0 +1,2 @@
export MIAB_LDAP_PROJECT=true
export LOCAL_MODS_DIR=/local

71
tests/vagrant/parallel.sh Executable file
View File

@ -0,0 +1,71 @@
#!/bin/bash
# Parallel provisioning for virtualbox because "The Vagrant VirtualBox
# provider does not support parallel execution at this time"
# (https://www.vagrantup.com/docs/providers/virtualbox/usage.html)
#
# Credit to:
# https://dzone.com/articles/parallel-provisioning-speeding
#
. "$(dirname "$0")/../lib/color-output.sh"
. "$(dirname "$0")/../lib/misc.sh"
OUTPUT_DIR=out
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
# set total parallel vms to (#cores minus 1)
MAX_PROCS=$(cat /proc/cpuinfo | grep processor | wc -l)
let MAX_PROCS-=1
parallel_provision() {
while read box; do
outfile="$OUTPUT_DIR/$box.out.txt"
echo "Provisioning '$box'. Output will be in: $outfile" 1>&2
echo $box
done | xargs -P $MAX_PROCS -I"BOXNAME" \
sh -c 'vagrant provision BOXNAME >'"$OUTPUT_DIR/"'BOXNAME.out.txt 2>&1 || echo "Error Occurred: BOXNAME"'
}
## -- main -- ##
start_time="$(date +%s)"
# start boxes sequentially to avoid vbox explosions
vagrant up --no-provision
# but run provision tasks in parallel
vagrant status | grep running | awk '{print $1}' | parallel_provision
# output overall result - Vagrantfile script must output "EXITCODE: <num>"
H1 "Results"
rc=0
for file in "$OUTPUT_DIR"/*.out.txt; do
box=$(basename $file | awk -F. '{print $1}')
exitcode="$(tail "$file" | grep EXITCODE: | awk '{print $NF}')"
echo -n "$box: "
if [ -z "$exitcode" ]; then
danger "NO EXITCODE!"
[ $rc -eq 0 ] && rc=2
elif [ "$exitcode" == "0" ]; then
success "SUCCESS"
else
danger "FAILURE ($exitcode)"
rc=1
fi
done
# output elapsed time
end_time="$(date +%s)"
echo ""
echo "Elapsed time: $(elapsed_pretty $start_time $end_time)"
# exit
echo ""
echo "Guest VMs are running! Destroy them with 'vagrant destroy -f'"
exit $rc

View File

@ -261,6 +261,10 @@ class UfwAllow(Grammar):
grammar = (ZERO_OR_MORE(SPACE), L("ufw_allow "), REST_OF_LINE, EOL)
def value(self):
return shell_line("ufw allow " + self[2].string)
class UfwLimit(Grammar):
grammar = (ZERO_OR_MORE(SPACE), L("ufw_limit "), REST_OF_LINE, EOL)
def value(self):
return shell_line("ufw limit " + self[2].string)
class RestartService(Grammar):
grammar = (ZERO_OR_MORE(SPACE), L("restart_service "), REST_OF_LINE, EOL)
def value(self):
@ -275,7 +279,7 @@ class OtherLine(Grammar):
return "<pre class='shell'><div>" + recode_bash(self.string.strip()) + "</div></pre>\n"
class BashElement(Grammar):
grammar = Comment | CatEOF | EchoPipe | EchoLine | HideOutput | EditConf | SedReplace | AptGet | UfwAllow | RestartService | OtherLine
grammar = Comment | CatEOF | EchoPipe | EchoLine | HideOutput | EditConf | SedReplace | AptGet | UfwAllow | UfwLimit | RestartService | OtherLine
def value(self):
return self[0].value()