diff options
| -rwxr-xr-x | desktop/modules/user/test-userctl.sh | 143 | ||||
| -rwxr-xr-x | desktop/modules/user/userctl | 115 |
2 files changed, 258 insertions, 0 deletions
diff --git a/desktop/modules/user/test-userctl.sh b/desktop/modules/user/test-userctl.sh new file mode 100755 index 0000000..cc90550 --- /dev/null +++ b/desktop/modules/user/test-userctl.sh @@ -0,0 +1,143 @@ +#!/bin/bash +# +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The one runnable check for userctl. busctl, kwriteconfig6, git and gpg are +# stubs on PATH that log their arguments, so nothing here reaches the real +# AccountsService, emaildefaults or git config. jq is the real one. +# +# Usage: ./test-userctl.sh (exit 0 = all passed) + +set -u + +here="$(cd "$(dirname "$0")" && pwd)" +ctl="$here/userctl" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +mkdir "$tmp/bin" +cat > "$tmp/bin/stub" <<'EOF' +#!/bin/bash +n="${0##*/}" +{ printf '%s' "$n"; printf ' [%s]' "$@"; echo; } >> "$LOG" +[[ "${FAIL:-}" == "$n" ]] && { echo boom >&2; exit 1; } +case "$n" in + busctl) [[ " $* " == *" get-property "* ]] && printf '{"type":"s","data":"%s"}\n' "${@: -1}" ;; + git) [[ " $* " == *" --get "* ]] && { [[ -n "${GIT_VALUE:-}" ]] && echo "$GIT_VALUE" || exit 1; } ;; + gpg) cat "$GPG_FIXTURE" ;; +esac +exit 0 +EOF +chmod +x "$tmp/bin/stub" +for n in busctl kwriteconfig6 git gpg; do ln -s stub "$tmp/bin/$n"; done +export PATH="$tmp/bin:$PATH" HOME="$tmp" LOG="$tmp/log" + +obj="/org/freedesktop/Accounts/User$(id -u)" +acc="busctl [--system] [call] [org.freedesktop.Accounts] [$obj] [org.freedesktop.Accounts.User]" + +pass=0 +fail=0 +check() { + local label="$1" want="$2" got="$3" + if [[ "$want" == "$got" ]]; then + printf 'ok %s\n' "$label"; pass=$((pass + 1)) + else + printf 'FAIL %s:\n want %q\n got %q\n' "$label" "$want" "$got"; fail=$((fail + 1)) + fi +} +reset() { : > "$LOG"; unset FAIL GIT_VALUE; } + +# get: one line per field, AccountsService through --json, the key from git. +reset +export GIT_VALUE=ABCDEF +check "get" "name=RealName +email=Email +avatar=IconFile +language=Language +location=Location +signingkey=ABCDEF" "$("$ctl" get)" + +# keys: the primary fingerprint and first uid of each secret key; subkey +# fingerprints and later uids are skipped. +reset +cat > "$tmp/gpg.txt" <<'EOF' +sec:u:255:22:AAAA1111:1700000000:::u:::scESC:::+:::23::0: +fpr:::::::::1111222233334444555566667777AAAA1111: +grp:::::::::0000: +uid:u::::1700000000::HASH::Jane Doèr <jane@example.org>::::::::::0: +uid:u::::1700000000::HASH::Jane D. <jd@example.org>::::::::::0: +ssb:u:255:18:SUB1:1700000000::::::e:::+:::23: +fpr:::::::::9999888877776666555544443333SUB1: +sec:u:255:22:BBBB2222:1700000000:::u:::scESC:::#:::23::0: +fpr:::::::::AAAABBBBCCCCDDDDEEEEFFFF0000BBBB2222: +uid:u::::1700000000::HASH::Work Key <work@example.org>::::::::::0: +EOF +export GPG_FIXTURE="$tmp/gpg.txt" +check "keys" "1111222233334444555566667777AAAA1111 Jane Doèr <jane@example.org> +AAAABBBBCCCCDDDDEEEEFFFF0000BBBB2222 Work Key <work@example.org>" "$("$ctl" keys)" + +# set name: AccountsService, emaildefaults, git with the surname shortened. +reset +out="$("$ctl" set name "Jane Doèr")" +check "set name prints nothing" "" "$out" +check "set name writes" "$acc [SetRealName] [s] [Jane Doèr] +kwriteconfig6 [--file] [emaildefaults] [--group] [PROFILE_Default] [--key] [FullName] [Jane Doèr] +git [config] [--global] [user.name] [Jane D.]" "$(cat "$LOG")" + +gitname() { reset; "$ctl" set name "$1" >/dev/null; grep '^git' "$LOG"; } +check "three words" "git [config] [--global] [user.name] [Anna Maria R.]" "$(gitname "Anna Maria Rossi")" +check "one word" "git [config] [--global] [user.name] [Cher]" "$(gitname "Cher")" +check "accented initial" "git [config] [--global] [user.name] [Ève À.]" "$(gitname "Ève Àlba")" +check "extra spaces" "git [config] [--global] [user.name] [Jane D.]" "$(gitname " Jane Doèr ")" + +# set email: the same three stores. +reset +"$ctl" set email jane@example.org >/dev/null +check "set email writes" "$acc [SetEmail] [s] [jane@example.org] +kwriteconfig6 [--file] [emaildefaults] [--group] [PROFILE_Default] [--key] [EmailAddress] [jane@example.org] +git [config] [--global] [user.email] [jane@example.org]" "$(cat "$LOG")" + +# AccountsService only. +for pair in avatar:SetIconFile language:SetLanguage location:SetLocation; do + reset + "$ctl" set "${pair%%:*}" v >/dev/null + check "set ${pair%%:*} writes" "$acc [${pair#*:}] [s] [v]" "$(cat "$LOG")" +done + +# signingkey: a fingerprint is written; empty unsets, but only if one is set. +reset +"$ctl" set signingkey ABCD >/dev/null +check "set signingkey" "git [config] [--global] [user.signingkey] [ABCD]" "$(cat "$LOG")" +reset +export GIT_VALUE=OLD +"$ctl" set signingkey "" >/dev/null +check "unset signingkey" "git [config] [--global] [--get] [user.signingkey] +git [config] [--global] [--unset] [user.signingkey]" "$(cat "$LOG")" +reset +"$ctl" set signingkey "" >/dev/null +check "unset when none is set" "git [config] [--global] [--get] [user.signingkey]" "$(cat "$LOG")" + +# A failing write stops there, says why on stdout, and exits 1. +reset +export FAIL=busctl +out="$("$ctl" set name "Jane Doèr")"; rc=$? +check "failure exit" "1" "$rc" +check "failure message" "busctl: boom" "$out" +check "failure stops" "1" "$(wc -l < "$LOG" | tr -d ' ')" + +# An unknown field is refused. +reset +"$ctl" set shoesize 42 >/dev/null 2>&1; rc=$? +check "unknown field" "2" "$rc" + +printf '\n%d passed, %d failed\n' "$pass" "$fail" +[[ $fail -eq 0 ]] diff --git a/desktop/modules/user/userctl b/desktop/modules/user/userctl new file mode 100755 index 0000000..07d3a63 --- /dev/null +++ b/desktop/modules/user/userctl @@ -0,0 +1,115 @@ +#!/bin/bash +# +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The user's details, read and written where other programs look for them. +# +# userctl get key=value: name email avatar language location signingkey +# userctl keys fingerprint<TAB>uid per gpg secret key +# userctl set <key> <val> write one field to every store holding it +# +# Stores: AccountsService (and through SetRealName the passwd GECOS that +# LibreOffice reads), KDE's ~/.config/emaildefaults, the global git config. +# Polkit lets a user change their own AccountsService entry without a prompt. +# +# set prints nothing on success. On a failing write it prints the command and +# its error on stdout, for the drawer page, and exits 1 without trying the +# remaining stores. A new field is one case below and one entry in the +# page's field list. + +set -u +# Character-wise ${x:0:1}, so an accented initial is not cut in half. +export LC_ALL=C.UTF-8 + +ACC=org.freedesktop.Accounts +OBJ="/org/freedesktop/Accounts/User$(id -u)" +IFACE=org.freedesktop.Accounts.User + +usage() { + printf 'usage: %s get | keys | set <name|email|avatar|language|location|signingkey> <value>\n' \ + "${0##*/}" >&2 + exit 2 +} + +run() { + local err + if ! err="$("$@" 2>&1 >/dev/null)"; then + printf '%s: %s\n' "$1" "${err:-failed}" + exit 1 + fi +} + +# --json, because plain get-property prints non-ASCII as octal escapes. +prop() { + busctl --system --json=short get-property "$ACC" "$OBJ" "$IFACE" "$1" | jq -r .data +} + +acc() { run busctl --system call "$ACC" "$OBJ" "$IFACE" "$1" s "$2"; } +kde() { run kwriteconfig6 --file emaildefaults --group PROFILE_Default --key "$1" "$2"; } + +# The surname as an initial, for commits: "Jane Doèr" -> "Jane D.". +gitname() { + local -a w + read -ra w <<< "$1" + local n=${#w[@]} + if (( n < 2 )); then printf '%s' "${w[*]}"; return; fi + printf '%s %s.' "${w[*]:0:n-1}" "${w[n-1]:0:1}" +} + +[[ $# -ge 1 ]] || usage + +case "$1" in + get) + printf 'name=%s\n' "$(prop RealName)" + printf 'email=%s\n' "$(prop Email)" + printf 'avatar=%s\n' "$(prop IconFile)" + printf 'language=%s\n' "$(prop Language)" + printf 'location=%s\n' "$(prop Location)" + printf 'signingkey=%s\n' "$(git config --global --get user.signingkey)" + ;; + keys) + # The first fpr after a sec is the primary key's; the first uid + # after that names it. ssb fingerprints come later and are skipped. + gpg --list-secret-keys --with-colons 2>/dev/null | awk -F: ' + $1 == "sec" { want = 1; next } + $1 == "fpr" && want == 1 { fpr = $10; want = 2; next } + $1 == "uid" && want == 2 { print fpr "\t" $10; want = 0 }' + ;; + set) + [[ $# -eq 3 ]] || usage + v="$3" + case "$2" in + name) + acc SetRealName "$v" + kde FullName "$v" + run git config --global user.name "$(gitname "$v")" + ;; + email) + acc SetEmail "$v" + kde EmailAddress "$v" + run git config --global user.email "$v" + ;; + avatar) acc SetIconFile "$v" ;; + language) acc SetLanguage "$v" ;; + location) acc SetLocation "$v" ;; + signingkey) + if [[ -n "$v" ]]; then + run git config --global user.signingkey "$v" + elif git config --global --get user.signingkey >/dev/null; then + run git config --global --unset user.signingkey + fi + ;; + *) usage ;; + esac + ;; + *) usage ;; +esac |
