From d7af161206a58e25190b8e95c91e2596d3a62366 Mon Sep 17 00:00:00 2001 From: librelad Date: Tue, 18 Aug 2026 21:27:42 +0100 Subject: [PATCH] feat(mattermost): user-management tools via mmctl local mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five Tools-tab actions: create account, list users, reset password, set admin, deactivate. Driven by mmctl --local, which talks to the server's unix socket rather than the REST API — no credentials to store, no token to expire, and it keeps working when the admin account is locked out or the site URL is wrong. mmctl is also the only route available: the v11 image is distroless with no shell at all, so every call has to be a direct exec of a binary. Two things found by running them: - `user promote` / `user demote` convert between GUEST and member accounts and have nothing to do with administrator rights. Granting system admin is `roles system-admin` / `roles member`. The first version used the wrong pair and failed with "Unable to convert the guest to regular user because is not a guest." - mmctl errors are multi-line: a summary line, then an indented bullet carrying the part that explains anything. Reporting the first line alone surfaced "1 error occurred:" and threw the reason away. Deactivate rather than delete, deliberately: Mattermost's delete is a permanent content purge, which is not something a single WebUI button should do. Co-Authored-By: Claude Opus 5 --- containers/mattermost/mattermost.config | 6 + .../mattermost/scripts/mattermost_auth.sh | 121 ++++++++++++++++++ .../mattermost/tools/mattermost.tools.json | 105 +++++++++++++++ .../tools/mattermost_create_account.sh | 10 ++ .../tools/mattermost_deactivate_user.sh | 6 + .../mattermost/tools/mattermost_list_users.sh | 5 + .../tools/mattermost_reset_password.sh | 8 ++ .../mattermost/tools/mattermost_set_admin.sh | 8 ++ .../source/files/arrays/function_manifest.sh | 36 ++++++ 9 files changed, 305 insertions(+) create mode 100644 containers/mattermost/scripts/mattermost_auth.sh create mode 100644 containers/mattermost/tools/mattermost.tools.json create mode 100644 containers/mattermost/tools/mattermost_create_account.sh create mode 100644 containers/mattermost/tools/mattermost_deactivate_user.sh create mode 100644 containers/mattermost/tools/mattermost_list_users.sh create mode 100644 containers/mattermost/tools/mattermost_reset_password.sh create mode 100644 containers/mattermost/tools/mattermost_set_admin.sh 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 "$@"; }