The Tools tab has an interactive modal: when a list_users task completes it parses the task log for EZ_USER lines and renders one row per account with reset / promote / delete buttons. All four new apps failed its contract in every respect, so running List Users produced log text and nothing else. - The marker is EZ_USER, tab-separated as email, username, roles. Matrix and Stoat emitted LP_USER in a different field order; Mattermost and Rocket.Chat emitted no marker at all. - Matrix and Stoat then consumed their own marker lines in the formatting loop and printed only the pretty version, so nothing reached the log to parse. - The row buttons look up tools by id: reset_password, set_admin, delete_user. The deactivate tools were named deactivate_user / disable_user, so no delete button rendered. - Prefill only fills a field named email or username. Rocket.Chat's and Stoat's identifier field was called user, so a row action would have opened with an empty box. - '-' placeholders are truthy, so the modal's `email || username` fallback picked '-' over the real username for accounts without an email (rocket.cat). The EZ_USER line now carries an empty string; '-' stays in the readable line. Mattermost's listing is rebuilt on `mmctl --json`, which carries roles and delete_at. The text listing has neither, and there is no --system-admin filter on user list, so every account was reported as a plain user. Two parsing notes that cost time: mmctl prints status lines both before and after the JSON, so it needs raw_decode rather than json.loads; and --per-page above 200 makes it emit a warning line ahead of the payload. The modal's delete button also stops asserting "Delete user" over whatever the tool actually does — it takes its label and icon from the tool, because most of these deactivate and Matrix cannot delete at all. Verified by replaying the modal's own parser over real tool output: 2 rows for Matrix, 4 for Mattermost, 3 for Rocket.Chat, with admin and deactivated states resolving correctly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
157 lines
6.4 KiB
Bash
157 lines
6.4 KiB
Bash
#!/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() {
|
|
# --json rather than the human format: it carries `roles` and `delete_at`,
|
|
# which the plain listing does not. There is no --system-admin filter on
|
|
# `user list` (only --inactive), so parsing the text output left every
|
|
# account looking like a plain user.
|
|
local out
|
|
# 200 is mmctl's maximum; asking for more makes it print a warning line
|
|
# BEFORE the JSON, which then fails to parse.
|
|
out=$(_mmctl --json user list --per-page 200)
|
|
_mmctlFailed "$out" "Listing users" && return 1
|
|
|
|
local rendered
|
|
rendered=$(printf '%s' "$out" | python3 -c "
|
|
import sys, json
|
|
# _mmctl folds stderr into stdout, and mmctl prints status lines such as
|
|
# 'There are 4 users on local instance' AFTER the JSON as well as warnings
|
|
# before it. raw_decode stops at the end of the first complete JSON value and
|
|
# ignores whatever trails it, which plain json.loads will not do.
|
|
raw = sys.stdin.read()
|
|
start = min([i for i in (raw.find('['), raw.find('{')) if i >= 0], default=-1)
|
|
users = []
|
|
if start >= 0:
|
|
try:
|
|
users, _ = json.JSONDecoder().raw_decode(raw[start:])
|
|
except Exception:
|
|
users = []
|
|
if isinstance(users, dict):
|
|
users = users.get('users', [])
|
|
for u in users:
|
|
roles = 'admin' if 'system_admin' in (u.get('roles') or '') else 'user'
|
|
if u.get('delete_at'):
|
|
roles += ',deactivated'
|
|
email = u.get('email') or '-'
|
|
name = u.get('username') or '-'
|
|
# EZ_USER<TAB>email<TAB>username<TAB>roles is what the WebUI user-list modal
|
|
# parses; the aligned line after it is for whoever reads the log.
|
|
print('EZ_USER\\t%s\\t%s\\t%s' % (email, name, roles))
|
|
print(' %-30s %-22s %s' % (email, name, roles))
|
|
print('LP_TOTAL:%d' % len(users))
|
|
" 2>/dev/null)
|
|
|
|
local line total=0
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
LP_TOTAL:*) total="${line#LP_TOTAL:}" ;;
|
|
*) [[ -n "$line" ]] && printf '%s\n' "$line" ;;
|
|
esac
|
|
done <<< "$rendered"
|
|
isSuccessful "$total 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."
|
|
}
|