feat(mattermost): user-management tools via mmctl local mode

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 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-18 21:27:42 +01:00
parent ca7fca5007
commit d7af161206
9 changed files with 305 additions and 0 deletions

View File

@ -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=

View File

@ -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."
}

View File

@ -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
}
]
}
]
}

View File

@ -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)"
}

View File

@ -0,0 +1,6 @@
#!/bin/bash
appMattermostDeactivateUser() {
local args="$1"
authAdapterCall mattermost deleteUser "$(authToolArg "$args" email)"
}

View File

@ -0,0 +1,5 @@
#!/bin/bash
appMattermostListUsers() {
authAdapterCall mattermost listUsers
}

View File

@ -0,0 +1,8 @@
#!/bin/bash
appMattermostResetPassword() {
local args="$1"
authAdapterCall mattermost setPassword \
"$(authToolArg "$args" email)" \
"$(authToolArg "$args" password)"
}

View File

@ -0,0 +1,8 @@
#!/bin/bash
appMattermostSetAdmin() {
local args="$1"
authAdapterCall mattermost setAdmin \
"$(authToolArg "$args" email)" \
"$(authToolArg "$args" admin)"
}

View File

@ -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 "$@"; }