diff --git a/.envrc.sample b/.envrc.sample index 597d314..3e86ea1 100644 --- a/.envrc.sample +++ b/.envrc.sample @@ -12,6 +12,25 @@ export MEDIAWIKI_LOGO_FILE='$wgResourceBasePath/resources/assets/wiki.png' export DB_ROOT_PASSWORD_VERSION=v1 export DB_PASSWORD_VERSION=v1 export MEDIAWIKI_SECRET_KEY_VERSION=v1 + export LOCAL_SETTINGS_CONF_VERSION=v1 export HTACCESS_CONF_VERSION=v1 export ENTRYPOINT_CONF_VERSION=v1 + +# SAML + +#export COMPOSE="compose.yml compose.simplesaml.yml" + +#export SAML_ENABLED=1 +#export SAML_CONTACT_NAME="Sam Ell" +#export SAML_CONTACT_EMAIL="saml@example.com" + +#export SAML_EMAIL_ATTRIBUTE=email +#export SAML_REAL_NAME_ATTRIBUTE=realname +#export SAML_AUTH_SOURCE_ID=dev-sp +#export SAML_USERNAME_ATTRIBUTE=user + +#export SAML_ADMIN_PASSWORD_VERSION=v1 +#export SAML_SECRET_SALT_VERSION=v1 + +#export SAML_ENTRYPOINT_CONF_VERSION=v1 diff --git a/LocalSettings.php.tmpl b/LocalSettings.php.tmpl index a015db0..67355c1 100644 --- a/LocalSettings.php.tmpl +++ b/LocalSettings.php.tmpl @@ -132,7 +132,6 @@ wfLoadSkin( 'MonoBook' ); wfLoadSkin( 'Timeless' ); wfLoadSkin( 'Vector' ); - # Enabled extensions. Most of the extensions are enabled by adding # wfLoadExtensions('ExtensionName'); # to LocalSettings.php. Check specific extension documentation for more details. @@ -166,3 +165,20 @@ $wgVirtualRestConfig['modules']['parsoid'] = [ // whether to parse URL as if they were meant for RESTBase (boolean or null, optional) 'restbaseCompat' => null, ]; + +{{ if eq (env "SAML_ENABLED") "1" }} +wfLoadExtension( 'PluggableAuth' ); + +wfLoadExtension( 'SimpleSAMLphp' ); + +$wgSimpleSAMLphp_InstallDir = "/var/simplesamlphp/"; +$wgSimpleSAMLphp_AuthSourceId = "{{ env "SAML_SERVICE_PROVIDER" }}"; +$wgSimpleSAMLphp_RealNameAttribute = "{{ env "SAML_REAL_NAME_ATTRIBUTE" }}"; +$wgSimpleSAMLphp_EmailAttribute = "{{ env "SAML_EMAIL_ATTRIBUTE" }}"; +$wgSimpleSAMLphp_UsernameAttribute = "{{ env "SAML_USERNAME_ATTRIBUTE" }}"; + +$wgGroupPermissions['*']['autocreateaccount'] = true; +$wgGroupPermissions['*']['createaccount'] = false; + +$wgDebugLogFile = "/var/log/debug-{$wgDBname}.log"; +{{ end }} diff --git a/README.md b/README.md index 1694cce..2f8227f 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,43 @@ Based on [`mediawiki-ve-bundle`][mediawiki-ve]. your Docker swarm box 4. `direnv allow` (or `. .envrc`) 5. `abra secret_generate db_password v1 && abra secret_generate db_root_password v2` -7. `abra secret_generate mediawiki_secret_key "pwgen -n 64 1"` -8. `abra deploy` -9. `abra service_run mediawiki /bin/bash` to open a shell -10. `php /var/www/html/maintenance/createAndPromote.php YourUsername YourPassword` +6. `abra secret_generate mediawiki_secret_key "pwgen -n 64 1"` +7. `abra deploy` +8. `abra service_run mediawiki /bin/bash` to open a shell +9. `php /var/www/html/maintenance/createAndPromote.php YourUsername YourPassword` + +## SimpleSAMLphp + +This app includes optional SAML Single Sign On using +[SimpleSAMLphp][simplesamlphp] and Mediawiki's +[Extension:SimpleSAMLphp][mw-simplesamlphp], based on the +[`venatorfox/simplesamlphp`][venatorfox-simplesamlphp] image. + +NOTE: currently, if you enable SAML then it'll disable Mediawiki's own user account +system. Patches to make this configurable are welcome! + +1. Edit `.envrc` and uncomment lines in the `SAML` section (including `COMPOSE`) +2. `direnv allow` +3. Generate secrets: + ``` + abra secret_generate saml_admin_password v1 + abra secret_generate saml_secret_key v1 "pwgen -n 64 1" + ``` +4. `abra deploy` +5. Copy your SimpleSAMLphp metadata and certificates to the container (assuming + you have local `metadata` and `cert` folders: + ``` + abra cp metadata simplesamlphp:/var/simplesamlphp/ + abra cp cert simplesamlphp:/var/simplesamlphp/ + ``` +6. You can log into SimpleSAMLphp using the password you generated at + https://$DOMAIN/simplesaml/ and test authentication +7. Edit SimpleSAMLphp's `config.php` and change `store.sql.dsn`: + ``` + abra run simplesamlphp vim + # find 'store.sql.dsn' and edit to: + # 'sqlite:/var/simplesamlphp/data/simplesamlphp.sq3' + ``` ## License @@ -26,3 +59,7 @@ MIT License [abra]: https://git.autonomic.zone/autonomic-cooperative/abra [compose-traefik]: https://git.autonomic.zone/compose-stacks/traefik [mediawiki-ve]: https://hub.docker.com/r/revianlabs/mediawiki-ve-bundle + +[simplesamlphp]: https://simplesamlphp.org/ +[mw-simplesamlphp]: https://www.mediawiki.org/wiki/Extension:SimpleSAMLphp +[venatorfox-simplesamlphp]: https://hub.docker.com/r/venatorfox/simplesamlphp diff --git a/compose.simplesaml.yml b/compose.simplesaml.yml new file mode 100644 index 0000000..6fc6182 --- /dev/null +++ b/compose.simplesaml.yml @@ -0,0 +1,80 @@ +--- +version: '3.8' + +services: + mediawiki: + volumes: + - 'simplesaml:/var/simplesamlphp/' + - 'simplesaml_log:/var/simplesamlphp/log' + environment: + - SAML_AUTH_SOURCE_ID=${SAML_AUTH_SOURCE_ID} + - SAML_EMAIL_ATTRIBUTE=${SAML_EMAIL_ATTRIBUTE} + - SAML_REAL_NAME_ATTRIBUTE=${SAML_REAL_NAME_ATTRIBUTE} + - SAML_SERVICE_PROVIDER=${SAML_SERVICE_PROVIDER} + - SAML_USERNAME_ATTRIBUTE=${SAML_USERNAME_ATTRIBUTE} + + simplesaml: + image: venatorfox/simplesamlphp:latest + secrets: + - saml_admin_password + - saml_secret_salt + environment: + - DOMAIN=${DOMAIN} + - CONFIG_BASEURLPATH=https://${DOMAIN}/simplesaml/ + - CONFIG_AUTHADMINPASSWORD_FILE=/run/secrets/saml_admin_password + - CONFIG_SECRETSALT_FILE=/run/secrets/saml_secret_salt + - CONFIG_TECHNICALCONTACT_NAME=${SAML_CONTACT_NAME} + - CONFIG_TECHNICALCONTACT_EMAIL=${SAML_CONTACT_EMAIL} + - CONFIG_SHOWERRORS=true + - CONFIG_ERRORREPORTING=true + - CONFIG_ADMINPROTECTINDEXPAGE=true + - CONFIG_LOGGINGLEVEL=INFO + - CONFIG_ENABLESAML20IDP=true + - CONFIG_STORETYPE=sql + #- CONFIG_MEMCACHESTOREPREFIX=simplesamlphp + #- CONFIG_MEMCACHESTORESERVERS= 'memcache_store.servers' => [\n [\n ['hostname' => 'memcached']\n ], + - OPENLDAP_TLS_REQCERT=allow + - MTA_NULLCLIENT=true + - POSTFIX_MYHOSTNAME=${DOMAIN} + - POSTFIX_MYORIGIN=$$mydomain + - POSTFIX_INETINTERFACES=loopback-only + - DOCKER_REDIRECTLOGS=false + # Required if DOCKER_REDIRECTLOGS=true + # tty: true + configs: + - source: entrypoint_saml_conf + target: /docker-entrypoint.simplesaml.sh + mode: 0555 + volumes: + - simplesaml:/var/simplesamlphp/ + - simplesaml_log:/var/simplesamlphp/log + networks: + - internal + - proxy + entrypoint: /docker-entrypoint.simplesaml.sh + #entrypoint: ["tail", "-f", "/dev/null"] + deploy: + labels: + - "traefik.enable=true" + - "traefik.http.services.${STACK_NAME}_simplesaml.loadbalancer.server.port=80" + - "traefik.http.routers.${STACK_NAME}_simplesaml.rule=(Host(`${DOMAIN}`) && PathPrefix(`/simplesaml`))" + - "traefik.http.routers.${STACK_NAME}_simplesaml.entrypoints=web-secure" + - "traefik.http.routers.${STACK_NAME}_simplesaml.tls.certresolver=${LETS_ENCRYPT_ENV}" + +volumes: + simplesaml: + simplesaml_log: + +secrets: + saml_admin_password: + name: ${STACK_NAME}_saml_admin_password_${SAML_ADMIN_PASSWORD_VERSION} + external: true + saml_secret_salt: + name: ${STACK_NAME}_saml_secret_salt_${SAML_SECRET_SALT_VERSION} + external: true + +configs: + entrypoint_saml_conf: + name: ${STACK_NAME}_entrypoint_saml_${SAML_ENTRYPOINT_CONF_VERSION} + file: entrypoint.simplesaml.sh.tmpl + template_driver: golang diff --git a/compose.yml b/compose.yml index 3374392..def3db6 100644 --- a/compose.yml +++ b/compose.yml @@ -22,6 +22,7 @@ services: delay: "60s" max_attempts: 3 window: 120s + mediawiki: image: 'revianlabs/mediawiki-ve-bundle' environment: @@ -31,6 +32,7 @@ services: - MEDIAWIKI_EMAIL_FROM=${MEDIAWIKI_EMAIL_FROM} - MEDIAWIKI_SITENAME=${MEDIAWIKI_SITENAME} - MEDIAWIKI_SITENAMESPACE=${MEDIAWIKI_SITENAMESPACE} + - SAML_ENABLED=${SAML_ENABLED} volumes: - 'mediawiki_images:/var/www/html/images' - 'parsoid:/usr/lib/parsoid' diff --git a/entrypoint.sh.tmpl b/entrypoint.sh.tmpl index 0845c31..005fe48 100755 --- a/entrypoint.sh.tmpl +++ b/entrypoint.sh.tmpl @@ -3,26 +3,44 @@ set -eu -o pipefail init_db() { - set -eu + set -eu + if ! type mysql > /dev/null 2>&1; then apt update && apt install -y mariadb-client + fi - PASSWORD=`cat /run/secrets/db_password` - TABLE_COUNT=$(mysql -u mediawiki --password="$PASSWORD" -h mariadb mediawiki -e "SELECT count(*) AS TOTAL FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mediawiki';" -N -B) + PASSWORD=`cat /run/secrets/db_password` + TABLE_COUNT=$(mysql -u mediawiki --password="$PASSWORD" -h mariadb mediawiki -e "SELECT count(*) AS TOTAL FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mediawiki';" -N -B) - if [[ "${TABLE_COUNT}" == "0" ]]; then - mysql -u mediawiki --password="$PASSWORD" -h mariadb mediawiki < /var/www/html/maintenance/tables.sql - else - php /var/www/html/maintenance/update.php - fi + if [[ "${TABLE_COUNT}" == "0" ]]; then + mysql -u mediawiki --password="$PASSWORD" -h mariadb mediawiki < /var/www/html/maintenance/tables.sql + else + php /var/www/html/maintenance/update.php + fi +} + +install_extensions() { + if [ ! -d /var/www/html/extensions/PluggableAuth ]; then + git clone --depth 1 -b REL1_32 \ + https://gerrit.wikimedia.org/r/p/mediawiki/extensions/PluggableAuth \ + /var/www/html/extensions/PluggableAuth + fi + + if [ ! -d /var/www/html/extensions/SimpleSAMLphp ]; then + git clone --depth 1 -b REL1_32 \ + https://gerrit.wikimedia.org/r/p/mediawiki/extensions/SimpleSAMLphp \ + /var/www/html/extensions/SimpleSAMLphp + fi } main() { - set -eu + set -eu - a2enmod rewrite + a2enmod rewrite - init_db + install_extensions + + init_db } main diff --git a/entrypoint.simplesaml.sh.tmpl b/entrypoint.simplesaml.sh.tmpl new file mode 100644 index 0000000..c1c8542 --- /dev/null +++ b/entrypoint.simplesaml.sh.tmpl @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -e + +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + local val="$def" + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(< "${!fileVar}")" + fi + export "$var"="$val" + unset "$fileVar" +} + +load_vars() { + file_env "CONFIG_AUTHADMINPASSWORD" + file_env "CONFIG_SECRETSALT" +} + +generate_certs() { + CERT_DIR=/var/simplesamlphp/cert + + if [ -f "$CERT_DIR/saml.crt" ] && [ -f "$CERT_DIR/saml.pem" ]; then + return + fi + + if ! type openssl > /dev/null 2>&1; then + yum install -q -y openssl + fi + + openssl req -newkey rsa:4096 -new -x509 \ + -days 3652 -nodes \ + -out "$CERT_DIR/saml.crt" \ + -keyout "$CERT_DIR/saml.pem" \ + -subj "/C=XX/ST=/L=/O=/OU=SimpleSAML/CN=${DOMAIN}" +} + +enable_plugins() { + touch /var/simplesamlphp/modules/cas/enable +} + +main() { + set -eu + + load_vars + + enable_plugins + + generate_certs +} + +main + +/init "$@"