diff --git a/containers/mattermost/mattermost.config b/containers/mattermost/mattermost.config index 9f994ad..db12942 100644 --- a/containers/mattermost/mattermost.config +++ b/containers/mattermost/mattermost.config @@ -74,3 +74,9 @@ CFG_MATTERMOST_NETWORK=default # - description: human-readable description of the service # CFG_MATTERMOST_PORT_1="mattermost-service|webui|random:8065|public|tcp|false|true|true|Web Interface||mattermost" + +# AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user) +CFG_MATTERMOST_AUTH_PROFILE=multi_user +# Email of the account the WebUI card advertises; set by whoever runs the setup +# wizard or the create-account tool, and kept in step by the reset-password tool. +CFG_MATTERMOST_ADMIN_EMAIL= diff --git a/containers/mattermost/scripts/mattermost_auth.sh b/containers/mattermost/scripts/mattermost_auth.sh new file mode 100644 index 0000000..2a8a9b1 --- /dev/null +++ b/containers/mattermost/scripts/mattermost_auth.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +# Mattermost user management, via mmctl in local mode. +# +# mmctl is the current CLI — not the long-deprecated `mattermost` binary that +# used to ship alongside it. It lives in the image at /usr/local/bin/mmctl and +# is the only reason these tools are possible at all: the v11 image is +# distroless, with no shell, so every call has to be a direct exec of a binary +# with no pipes, redirects or shell built-ins available. +# +# --local talks to the server's unix socket instead of the REST API, which means +# no credentials to store, no token to expire, and it keeps working even when +# the admin account is locked out or the site URL is wrong. + +_mmctl() { + runFileOp docker exec -i mattermost-service mmctl --local "$@" 2>&1 +} + +# mmctl exits non-zero on failure and writes the reason to stderr, which _mmctl +# folds into stdout. +# +# Its errors are multi-line — a summary ("Error: 1 error occurred:") followed by +# an indented bullet carrying the part that actually explains anything. Reporting +# only the first line threw the useful half away, so prefer the bullet when +# there is one. +_mmctlFailed() { + local out="$1" what="$2" + [[ "$out" != *"Error:"* ]] && return 1 + local detail + detail=$(printf '%s' "$out" | sed -n 's/^[[:space:]]*\*[[:space:]]*//p' | head -1) + [[ -z "$detail" ]] && detail=$(printf '%s' "$out" | grep -m1 'Error:' | sed 's/.*Error: *//') + isError "$what failed: $detail" + return 0 +} + +authAdapter_mattermost_createUser() { + local email="$1" password="$2" username="$3" isAdmin="$4" + [[ -z "$email" ]] && { isError "An email address is required."; return 1; } + [[ -z "$username" ]] && username="${email%@*}" + [[ -z "$password" ]] && password=$(generateRandomPassword) + + # Mattermost usernames are lowercase and restricted to letters, numbers and + # . - _ — sanitise rather than let the server reject the whole call. + username=$(printf '%s' "$username" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9._-' '-' | sed 's/^-*//; s/-*$//') + [[ -z "$username" ]] && username="user" + + local out + out=$(_mmctl user create --email "$email" --username "$username" --password "$password") + _mmctlFailed "$out" "Creating $email" && return 1 + + if [[ "$isAdmin" == "true" ]]; then + local promote + promote=$(_mmctl roles system-admin "$email") + _mmctlFailed "$promote" "Granting system admin to $email" && return 1 + fi + + isSuccessful "Mattermost user created — Email: $email — Username: $username — Password: $password" +} + +authAdapter_mattermost_setPassword() { + local email="$1" password="$2" + [[ -z "$email" ]] && { isError "An email address is required."; return 1; } + [[ -z "$password" ]] && password=$(generateRandomPassword) + + local out + out=$(_mmctl user change-password "$email" --password "$password") + _mmctlFailed "$out" "Resetting $email" && return 1 + + # Keep the config in step if this is the account the WebUI card advertises. + [[ "$email" == "${CFG_MATTERMOST_ADMIN_EMAIL:-}" ]] && authPersistCfg mattermost ADMIN_PASSWORD "$password" + + isSuccessful "Mattermost password set for $email — New password: $password" +} + +authAdapter_mattermost_listUsers() { + local out + out=$(_mmctl user list --per-page 500) + _mmctlFailed "$out" "Listing users" && return 1 + + # `user list` prints "id: username (email)" per line, plus a trailing count. + local line count=0 + while IFS= read -r line; do + [[ "$line" =~ ^[a-z0-9]+:\ ]] || continue + local rest="${line#*: }" + printf ' %s\n' "$rest" + ((count++)) + done <<< "$out" + isSuccessful "$count Mattermost account(s)." +} + +# Mattermost distinguishes deactivate (reversible, frees nothing) from delete +# (permanent, purges content). This is the reversible one: it is what the +# product itself recommends, and a real delete is not undoable from a WebUI +# button click. +authAdapter_mattermost_deleteUser() { + local email="$1" + [[ -z "$email" ]] && { isError "An email address is required."; return 1; } + + local out + out=$(_mmctl user deactivate "$email") + _mmctlFailed "$out" "Deactivating $email" && return 1 + isSuccessful "Mattermost user '$email' deactivated. Re-enable them from the System Console if needed." +} + +authAdapter_mattermost_setAdmin() { + local email="$1" isAdmin="$2" + [[ -z "$email" ]] && { isError "An email address is required."; return 1; } + + # `roles system-admin` / `roles member`, NOT `user promote` / `user demote`: + # those two convert between guest and member accounts and have no bearing on + # administrator rights at all. + local out target="false" + if [[ "$isAdmin" == "true" ]]; then + target="true" + out=$(_mmctl roles system-admin "$email") + else + out=$(_mmctl roles member "$email") + fi + _mmctlFailed "$out" "Changing admin status for $email" && return 1 + isSuccessful "Mattermost user '$email' system admin → $target." +} diff --git a/containers/mattermost/tools/mattermost.tools.json b/containers/mattermost/tools/mattermost.tools.json new file mode 100644 index 0000000..afe553e --- /dev/null +++ b/containers/mattermost/tools/mattermost.tools.json @@ -0,0 +1,105 @@ +{ + "tools": [ + { + "id": "create_account", + "category": "users", + "label": "Create User Account", + "description": "Add a Mattermost user. Tick \"Make system admin\" for full rights.", + "icon": "👤", + "fields": [ + { + "name": "email", + "label": "Email", + "type": "text", + "placeholder": "user@example.com", + "required": true + }, + { + "name": "username", + "label": "Username", + "type": "text", + "placeholder": "Leave blank to derive from the email" + }, + { + "name": "password", + "label": "Password", + "type": "password", + "placeholder": "Leave blank to generate" + }, + { + "name": "admin", + "label": "Make system admin", + "type": "checkbox", + "default": false + } + ] + }, + { + "id": "list_users", + "category": "users", + "label": "List Users", + "description": "Every account on this server.", + "icon": "📋", + "fields": [] + }, + { + "id": "reset_password", + "category": "users", + "label": "Reset User Password", + "description": "Set a new password for an existing user.", + "icon": "🔑", + "fields": [ + { + "name": "email", + "label": "User email", + "type": "text", + "required": true + }, + { + "name": "password", + "label": "New password", + "type": "password", + "placeholder": "Leave blank to generate" + } + ] + }, + { + "id": "set_admin", + "category": "users", + "label": "Set Admin Status", + "description": "Promote or demote a system administrator.", + "icon": "👑", + "fields": [ + { + "name": "email", + "label": "User email", + "type": "text", + "required": true + }, + { + "name": "admin", + "label": "Make system admin", + "type": "checkbox", + "default": false + } + ] + }, + { + "id": "deactivate_user", + "category": "users", + "label": "Deactivate User Account", + "description": "Revoke access without deleting content. Reversible from the System Console.", + "icon": "🚫", + "destructive": true, + "confirm": "The user will be signed out and unable to log in.", + "fields": [ + { + "name": "email", + "label": "User email", + "type": "text", + "required": true + } + ] + } + ] +} diff --git a/containers/mattermost/tools/mattermost_create_account.sh b/containers/mattermost/tools/mattermost_create_account.sh new file mode 100644 index 0000000..d8de91c --- /dev/null +++ b/containers/mattermost/tools/mattermost_create_account.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +appMattermostCreateAccount() { + local args="$1" + authAdapterCall mattermost createUser \ + "$(authToolArg "$args" email)" \ + "$(authToolArg "$args" password)" \ + "$(authToolArg "$args" username)" \ + "$(authToolArg "$args" admin)" +} diff --git a/containers/mattermost/tools/mattermost_deactivate_user.sh b/containers/mattermost/tools/mattermost_deactivate_user.sh new file mode 100644 index 0000000..405c3af --- /dev/null +++ b/containers/mattermost/tools/mattermost_deactivate_user.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +appMattermostDeactivateUser() { + local args="$1" + authAdapterCall mattermost deleteUser "$(authToolArg "$args" email)" +} diff --git a/containers/mattermost/tools/mattermost_list_users.sh b/containers/mattermost/tools/mattermost_list_users.sh new file mode 100644 index 0000000..5fab9ef --- /dev/null +++ b/containers/mattermost/tools/mattermost_list_users.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +appMattermostListUsers() { + authAdapterCall mattermost listUsers +} diff --git a/containers/mattermost/tools/mattermost_reset_password.sh b/containers/mattermost/tools/mattermost_reset_password.sh new file mode 100644 index 0000000..5de9700 --- /dev/null +++ b/containers/mattermost/tools/mattermost_reset_password.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +appMattermostResetPassword() { + local args="$1" + authAdapterCall mattermost setPassword \ + "$(authToolArg "$args" email)" \ + "$(authToolArg "$args" password)" +} diff --git a/containers/mattermost/tools/mattermost_set_admin.sh b/containers/mattermost/tools/mattermost_set_admin.sh new file mode 100644 index 0000000..c974e98 --- /dev/null +++ b/containers/mattermost/tools/mattermost_set_admin.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +appMattermostSetAdmin() { + local args="$1" + authAdapterCall mattermost setAdmin \ + "$(authToolArg "$args" email)" \ + "$(authToolArg "$args" admin)" +} diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index 64a4337..38df2d2 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -62,6 +62,11 @@ declare -gA LP_FN_MAP=( [appMatrixListUsers]="matrix/tools/matrix_list_users.sh" [appMatrixResetPassword]="matrix/tools/matrix_reset_password.sh" [appMatrixSetAdmin]="matrix/tools/matrix_set_admin.sh" + [appMattermostCreateAccount]="mattermost/tools/mattermost_create_account.sh" + [appMattermostDeactivateUser]="mattermost/tools/mattermost_deactivate_user.sh" + [appMattermostListUsers]="mattermost/tools/mattermost_list_users.sh" + [appMattermostResetPassword]="mattermost/tools/mattermost_reset_password.sh" + [appMattermostSetAdmin]="mattermost/tools/mattermost_set_admin.sh" [appNetworkApplyMode_gluetun]="gluetun/scripts/gluetun_network.sh" [appNetworkRegisterPorts_gluetun]="gluetun/scripts/gluetun_network.sh" [appNextcloudAddTrustedDomain]="nextcloud/tools/nextcloud_add_trusted_domain.sh" @@ -158,6 +163,11 @@ declare -gA LP_FN_MAP=( [authAdapter_matrix_listUsers]="matrix/scripts/matrix_auth.sh" [authAdapter_matrix_setAdmin]="matrix/scripts/matrix_auth.sh" [authAdapter_matrix_setPassword]="matrix/scripts/matrix_auth.sh" + [authAdapter_mattermost_createUser]="mattermost/scripts/mattermost_auth.sh" + [authAdapter_mattermost_deleteUser]="mattermost/scripts/mattermost_auth.sh" + [authAdapter_mattermost_listUsers]="mattermost/scripts/mattermost_auth.sh" + [authAdapter_mattermost_setAdmin]="mattermost/scripts/mattermost_auth.sh" + [authAdapter_mattermost_setPassword]="mattermost/scripts/mattermost_auth.sh" [authAdapter_nextcloud_createUser]="nextcloud/scripts/nextcloud_auth.sh" [authAdapter_nextcloud_deleteUser]="nextcloud/scripts/nextcloud_auth.sh" [authAdapter_nextcloud_listUsers]="nextcloud/scripts/nextcloud_auth.sh" @@ -690,6 +700,8 @@ declare -gA LP_FN_MAP=( [migrateRunHook]="migrate/migrate_hooks.sh" [migrateSystem]="migrate/migrate_apply.sh" [migrateUrlRewriteEnabled]="migrate/migrate_url_rewrite.sh" + [_mmctl]="mattermost/scripts/mattermost_auth.sh" + [_mmctlFailed]="mattermost/scripts/mattermost_auth.sh" [moneyapp_install_post_compose]="moneyapp/scripts/moneyapp_install_hooks.sh" [moneyapp_install_pre]="moneyapp/scripts/moneyapp_install_hooks.sh" [monitoringAppEnabled]="network/monitoring/monitoring.sh" @@ -1136,6 +1148,11 @@ declare -gA LP_FN_ROOT=( [appMatrixListUsers]="containers" [appMatrixResetPassword]="containers" [appMatrixSetAdmin]="containers" + [appMattermostCreateAccount]="containers" + [appMattermostDeactivateUser]="containers" + [appMattermostListUsers]="containers" + [appMattermostResetPassword]="containers" + [appMattermostSetAdmin]="containers" [appNetworkApplyMode_gluetun]="containers" [appNetworkRegisterPorts_gluetun]="containers" [appNextcloudAddTrustedDomain]="containers" @@ -1232,6 +1249,11 @@ declare -gA LP_FN_ROOT=( [authAdapter_matrix_listUsers]="containers" [authAdapter_matrix_setAdmin]="containers" [authAdapter_matrix_setPassword]="containers" + [authAdapter_mattermost_createUser]="containers" + [authAdapter_mattermost_deleteUser]="containers" + [authAdapter_mattermost_listUsers]="containers" + [authAdapter_mattermost_setAdmin]="containers" + [authAdapter_mattermost_setPassword]="containers" [authAdapter_nextcloud_createUser]="containers" [authAdapter_nextcloud_deleteUser]="containers" [authAdapter_nextcloud_listUsers]="containers" @@ -1764,6 +1786,8 @@ declare -gA LP_FN_ROOT=( [migrateRunHook]="scripts" [migrateSystem]="scripts" [migrateUrlRewriteEnabled]="scripts" + [_mmctl]="containers" + [_mmctlFailed]="containers" [moneyapp_install_post_compose]="containers" [moneyapp_install_pre]="containers" [monitoringAppEnabled]="scripts" @@ -2245,6 +2269,11 @@ appMatrixDeactivateUser() { unset -f appMatrixDeactivateUser; __lpAutoload "${in appMatrixListUsers() { unset -f appMatrixListUsers; __lpAutoload "${install_containers_dir}matrix/tools/matrix_list_users.sh"; appMatrixListUsers "$@"; } appMatrixResetPassword() { unset -f appMatrixResetPassword; __lpAutoload "${install_containers_dir}matrix/tools/matrix_reset_password.sh"; appMatrixResetPassword "$@"; } appMatrixSetAdmin() { unset -f appMatrixSetAdmin; __lpAutoload "${install_containers_dir}matrix/tools/matrix_set_admin.sh"; appMatrixSetAdmin "$@"; } +appMattermostCreateAccount() { unset -f appMattermostCreateAccount; __lpAutoload "${install_containers_dir}mattermost/tools/mattermost_create_account.sh"; appMattermostCreateAccount "$@"; } +appMattermostDeactivateUser() { unset -f appMattermostDeactivateUser; __lpAutoload "${install_containers_dir}mattermost/tools/mattermost_deactivate_user.sh"; appMattermostDeactivateUser "$@"; } +appMattermostListUsers() { unset -f appMattermostListUsers; __lpAutoload "${install_containers_dir}mattermost/tools/mattermost_list_users.sh"; appMattermostListUsers "$@"; } +appMattermostResetPassword() { unset -f appMattermostResetPassword; __lpAutoload "${install_containers_dir}mattermost/tools/mattermost_reset_password.sh"; appMattermostResetPassword "$@"; } +appMattermostSetAdmin() { unset -f appMattermostSetAdmin; __lpAutoload "${install_containers_dir}mattermost/tools/mattermost_set_admin.sh"; appMattermostSetAdmin "$@"; } appNetworkApplyMode_gluetun() { unset -f appNetworkApplyMode_gluetun; __lpAutoload "${install_containers_dir}gluetun/scripts/gluetun_network.sh"; appNetworkApplyMode_gluetun "$@"; } appNetworkRegisterPorts_gluetun() { unset -f appNetworkRegisterPorts_gluetun; __lpAutoload "${install_containers_dir}gluetun/scripts/gluetun_network.sh"; appNetworkRegisterPorts_gluetun "$@"; } appNextcloudAddTrustedDomain() { unset -f appNextcloudAddTrustedDomain; __lpAutoload "${install_containers_dir}nextcloud/tools/nextcloud_add_trusted_domain.sh"; appNextcloudAddTrustedDomain "$@"; } @@ -2341,6 +2370,11 @@ authAdapter_matrix_deleteUser() { unset -f authAdapter_matrix_deleteUser; __lpAu authAdapter_matrix_listUsers() { unset -f authAdapter_matrix_listUsers; __lpAutoload "${install_containers_dir}matrix/scripts/matrix_auth.sh"; authAdapter_matrix_listUsers "$@"; } authAdapter_matrix_setAdmin() { unset -f authAdapter_matrix_setAdmin; __lpAutoload "${install_containers_dir}matrix/scripts/matrix_auth.sh"; authAdapter_matrix_setAdmin "$@"; } authAdapter_matrix_setPassword() { unset -f authAdapter_matrix_setPassword; __lpAutoload "${install_containers_dir}matrix/scripts/matrix_auth.sh"; authAdapter_matrix_setPassword "$@"; } +authAdapter_mattermost_createUser() { unset -f authAdapter_mattermost_createUser; __lpAutoload "${install_containers_dir}mattermost/scripts/mattermost_auth.sh"; authAdapter_mattermost_createUser "$@"; } +authAdapter_mattermost_deleteUser() { unset -f authAdapter_mattermost_deleteUser; __lpAutoload "${install_containers_dir}mattermost/scripts/mattermost_auth.sh"; authAdapter_mattermost_deleteUser "$@"; } +authAdapter_mattermost_listUsers() { unset -f authAdapter_mattermost_listUsers; __lpAutoload "${install_containers_dir}mattermost/scripts/mattermost_auth.sh"; authAdapter_mattermost_listUsers "$@"; } +authAdapter_mattermost_setAdmin() { unset -f authAdapter_mattermost_setAdmin; __lpAutoload "${install_containers_dir}mattermost/scripts/mattermost_auth.sh"; authAdapter_mattermost_setAdmin "$@"; } +authAdapter_mattermost_setPassword() { unset -f authAdapter_mattermost_setPassword; __lpAutoload "${install_containers_dir}mattermost/scripts/mattermost_auth.sh"; authAdapter_mattermost_setPassword "$@"; } authAdapter_nextcloud_createUser() { unset -f authAdapter_nextcloud_createUser; __lpAutoload "${install_containers_dir}nextcloud/scripts/nextcloud_auth.sh"; authAdapter_nextcloud_createUser "$@"; } authAdapter_nextcloud_deleteUser() { unset -f authAdapter_nextcloud_deleteUser; __lpAutoload "${install_containers_dir}nextcloud/scripts/nextcloud_auth.sh"; authAdapter_nextcloud_deleteUser "$@"; } authAdapter_nextcloud_listUsers() { unset -f authAdapter_nextcloud_listUsers; __lpAutoload "${install_containers_dir}nextcloud/scripts/nextcloud_auth.sh"; authAdapter_nextcloud_listUsers "$@"; } @@ -2873,6 +2907,8 @@ _migrateResolveLocation() { unset -f _migrateResolveLocation; __lpAutoload "${in migrateRunHook() { unset -f migrateRunHook; __lpAutoload "${install_scripts_dir}migrate/migrate_hooks.sh"; migrateRunHook "$@"; } migrateSystem() { unset -f migrateSystem; __lpAutoload "${install_scripts_dir}migrate/migrate_apply.sh"; migrateSystem "$@"; } migrateUrlRewriteEnabled() { unset -f migrateUrlRewriteEnabled; __lpAutoload "${install_scripts_dir}migrate/migrate_url_rewrite.sh"; migrateUrlRewriteEnabled "$@"; } +_mmctl() { unset -f _mmctl; __lpAutoload "${install_containers_dir}mattermost/scripts/mattermost_auth.sh"; _mmctl "$@"; } +_mmctlFailed() { unset -f _mmctlFailed; __lpAutoload "${install_containers_dir}mattermost/scripts/mattermost_auth.sh"; _mmctlFailed "$@"; } moneyapp_install_post_compose() { unset -f moneyapp_install_post_compose; __lpAutoload "${install_containers_dir}moneyapp/scripts/moneyapp_install_hooks.sh"; moneyapp_install_post_compose "$@"; } moneyapp_install_pre() { unset -f moneyapp_install_pre; __lpAutoload "${install_containers_dir}moneyapp/scripts/moneyapp_install_hooks.sh"; moneyapp_install_pre "$@"; } monitoringAppEnabled() { unset -f monitoringAppEnabled; __lpAutoload "${install_scripts_dir}network/monitoring/monitoring.sh"; monitoringAppEnabled "$@"; }