#!/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_USERemailusernameroles 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." }