change password Core

This commit is contained in:
2026-10-30 06:48:02 -06:00
parent a3a980579c
commit 14d2bd4cd5
4 changed files with 161 additions and 7 deletions
+1 -1
View File
@@ -216,7 +216,7 @@ configure
It covers:
- **Your Federated Computer Core.** Give it your Superpanel's address (e.g. https://superpanel.example.com), your Core email, name and password, and it adds Superpanel to your applications, adds your Nextcloud (calendar, contacts and files) and email to GNOME's Online Accounts, and mounts your Nextcloud files at /mnt/example.com ("example.com" in the Files sidebar), and points Element at your Core's Matrix server. For Nextcloud and the files it creates a Nextcloud app password, which you can revoke in Nextcloud under Settings > Security. You can add more Cores: run `configure core` again and choose "Add a Core". Each gets its own Superpanel, Nextcloud, email and files; Element, Tailscale and backups use your main Core (the first one, unless you choose another). "Remove a Core" takes it off this computer and revokes its app password, and leaves everything on the Core itself as it is.
- **Your Federated Computer Core.** Give it your Superpanel's address (e.g. https://superpanel.example.com), your Core email, name and password, and it adds Superpanel to your applications, adds your Nextcloud (calendar, contacts and files) and email to GNOME's Online Accounts, and mounts your Nextcloud files at /mnt/example.com ("example.com" in the Files sidebar), and points Element at your Core's Matrix server. For Nextcloud and the files it creates a Nextcloud app password, which you can revoke in Nextcloud under Settings > Security. You can add more Cores: run `configure core` again and choose "Add a Core". Each gets its own Superpanel, Nextcloud, email and files; Element, Tailscale and backups use your main Core (the first one, unless you choose another). "Remove a Core" takes it off this computer and revokes its app password, and leaves everything on the Core itself as it is. If you change your Core password (e.g. in Superpanel), run `configure core` and choose "Your Core password changed": email keeps your Core password, so it saves the new one there, and it signs in to Nextcloud with it, which turns the app passwords for Nextcloud, files and backups back on (Nextcloud turns them off when your password changes), then checks each of them.
- **Your Core's Tailscale network.** Joins this computer to it through headscale.example.com, so you can reach your other machines on it. Set up your Core first. You sign in through a link it shows you; if this computer is on another Tailscale network, it moves it to your Core's. To leave your Core's network, run `configure tailscale` again.
- **Daily backups of your home folder** to your Nextcloud files, in a .backup-{machine name} folder, with restic. Set up your Core first. The backup is encrypted with a passphrase that `configure` shows you once: save it somewhere other than this computer, such as Bitwarden. It keeps 7 daily, 4 weekly and 6 monthly backups, and leaves out caches, things that can be reinstalled, and WinBoat's Windows VM (~/winboat). To leave out more, list paths in ~/.config/coredesktop/backup-exclude, one per line. restic reaches Nextcloud directly through rclone with its own Nextcloud app password, so the backup doesn't depend on the /mnt mount. See "Restoring from a backup" below. To turn backups off, run `configure backup` again; it also offers to delete the backups from your Nextcloud files (they go to Nextcloud's trash bin).
- Signing in to Claude Code, opencode and Hermes Agent and choosing their model providers.
+67
View File
@@ -348,6 +348,69 @@ setup_core() {
return $STATUS
}
# Check an app password this computer keeps for the Core
app_password_works() {
local title="$1" app_password="$2"
[ -n "$app_password" ] || return 0
case "$(nextcloud_sign_in "$app_password"; echo $?)" in
0) echo " $title: working" ;;
2) echo -e " \e[31m$title: Nextcloud doesn't accept its app password.\e[0m"
echo " It may have been revoked in Nextcloud under Settings > Security."
return 1 ;;
*) echo " $title: couldn't reach Nextcloud to check" ;;
esac
}
# After your Core password changes, e.g. in Superpanel. Email keeps your Core
# password, so it gets the new one. Nextcloud, files and backups use app
# passwords, which stay the same; Nextcloud turns them off until you sign in
# with the new password, and checking the password here does that.
change_password() {
local tries reached=1
load_core "$1"
STATUS=0
for tries in 1 2 3; do
read -rsp "Your new Core password: " PASSWORD
echo
nextcloud_sign_in
case $? in
0) break ;;
2) echo "Nextcloud says that password is wrong."; PASSWORD="" ;;
*) echo "Couldn't reach Nextcloud at $NEXTCLOUD; trying the password for email anyway."
reached=0; break ;;
esac
done
[ -n "$PASSWORD" ] || return 1
echo
if email_done; then
jq -n --arg password "$PASSWORD" '{"imap-password": $password, "smtp-password": $password}' \
| "$GOA_ACCOUNT" update imap_smtp "$CORE_EMAIL" 2> /dev/null
case $? in
0) echo " Email: has your new password" ;;
3) echo -e " \e[31mEmail: saved your new password, but the mail server didn't accept it.\e[0m"
STATUS=1 ;;
*) echo -e " \e[31mEmail: couldn't save your new password.\e[0m"; STATUS=1 ;;
esac
fi
unset PASSWORD
[ $reached = 1 ] || return 1
if nextcloud_done; then
if "$GOA_ACCOUNT" check owncloud "$CORE_USER@nextcloud.$CORE_DOMAIN" 2> /dev/null; then
echo " Nextcloud in Online Accounts: working"
else
echo -e " \e[31mNextcloud in Online Accounts: can't sign in.\e[0m"
echo " Remove it in Settings > Online Accounts, then run: configure core"
STATUS=1
fi
fi
files_done && { app_password_works "Files at $MOUNT" "$(files_app_password)" || STATUS=1; }
app_password_works "Backups" \
"$(secret-tool lookup coredesktop backup item nextcloud url "$WEBDAV" 2> /dev/null)" || STATUS=1
return $STATUS
}
# Tailscale and Backup point at one Core, which can't be removed while they do
tailscale_on() {
[ "$(source "$TAILSCALE_CONF" 2> /dev/null && echo "$TAILSCALE_DOMAIN")" = "$1" ] \
@@ -473,6 +536,7 @@ run_configure() {
echo " Choose a number to finish or change its setup, or:"
echo " a) Add a Core"
[ ${#DOMAINS[@]} -gt 1 ] && echo " m) Make another Core the main one"
echo " p) Your Core password changed"
echo " r) Remove a Core"
echo " q) Leave it as it is"
echo
@@ -485,6 +549,9 @@ run_configure() {
[ ${#DOMAINS[@]} -gt 1 ] || return 0
pick_core "Which should be the main one?" "$MAIN_DOMAIN" || { echo "Choose a number from the list."; return 1; }
make_main "$PICKED" && main_changed ;;
p|P)
pick_core "Which Core's password changed?" || { echo "Choose a number from the list."; return 1; }
change_password "$PICKED" ;;
r|R)
pick_core "Which Core should be removed?" || { echo "Choose a number from the list."; return 1; }
remove_core "$PICKED" ;;
+78 -6
View File
@@ -1,19 +1,27 @@
#!/usr/bin/env python3
"""Add or remove an account in GNOME Online Accounts, the way Settings does.
"""Add, change or remove an account in GNOME Online Accounts, the way Settings does.
Settings adds accounts by calling AddAccount on the Online Accounts service,
which saves the account and keeps its password in the GNOME keyring. This
calls the same method. The account comes on stdin rather than as arguments,
so its password doesn't show up in the process list.
calls the same method. The service can't change a password, so when you sign
in again, Settings saves the new one in the keyring itself and asks the
service to check it (EnsureCredentials); update does the same. Passwords come
on stdin rather than as arguments, so they don't show up in the process list.
Usage:
goa-account exists <provider> <presentation-identity>
goa-account add < account.json
goa-account update <provider> <presentation-identity> < credentials.json
goa-account check <provider> <presentation-identity>
goa-account remove <provider> <presentation-identity>
account.json:
{"provider": "owncloud", "identity": "...", "presentation_identity": "...",
"credentials": {"password": "..."}, "details": {"Uri": "...", ...}}
credentials.json is the account's "credentials", e.g. {"password": "..."}.
check signs in with what's saved, and fails if the server says no. update
exits with 3 if it saved the password but the server didn't accept it.
"""
import json
@@ -22,10 +30,16 @@ import sys
import gi
gi.require_version("Gio", "2.0")
from gi.repository import Gio, GLib # noqa: E402
gi.require_version("Secret", "1")
from gi.repository import Gio, GLib, Secret # noqa: E402
BUS_NAME = "org.gnome.OnlineAccounts"
# How Online Accounts keeps passwords in the keyring: the credentials printed
# as a GVariant, under "<provider>:gen<N>:<account id>"
SCHEMA = Secret.Schema.new("org.gnome.OnlineAccounts", Secret.SchemaFlags.DONT_MATCH_NAME,
{"goa-identity": Secret.SchemaAttributeType.STRING})
def call(bus, path, interface, method, parameters, reply_type):
return bus.call_sync(BUS_NAME, path, interface, method, parameters,
@@ -43,12 +57,16 @@ def accounts(bus):
yield path, account
def find(bus, provider, presentation_identity):
return [path for path, account in accounts(bus)
def find_accounts(bus, provider, presentation_identity):
return [(path, account) for path, account in accounts(bus)
if account.get("ProviderType") == provider
and account.get("PresentationIdentity") == presentation_identity]
def find(bus, provider, presentation_identity):
return [path for path, _ in find_accounts(bus, provider, presentation_identity)]
def exists(bus, provider, presentation_identity):
return bool(find(bus, provider, presentation_identity))
@@ -63,6 +81,52 @@ def add(bus, account):
"org.gnome.OnlineAccounts.Manager", "AddAccount", parameters, "(o)")
def ensure_credentials(bus, path):
call(bus, path, "org.gnome.OnlineAccounts.Account", "EnsureCredentials", None, "(i)")
# N in the keyring's name comes from the provider, so this saves over the
# account's own item rather than making one
def store_credentials(provider, account_id, credentials):
items = Secret.password_search_sync(
None, {"xdg:schema": "org.gnome.OnlineAccounts"},
Secret.SearchFlags.ALL | Secret.SearchFlags.UNLOCK, None)
identities = [item.get_attributes().get("goa-identity", "") for item in items]
identities = [identity for identity in identities
if identity.startswith(f"{provider}:gen") and identity.endswith(f":{account_id}")]
if not identities:
raise LookupError(f"there's no saved password for {account_id} in the keyring")
secret = GLib.Variant("a{sv}", {key: GLib.Variant("s", value)
for key, value in credentials.items()}).print_(True)
for identity in identities:
Secret.password_store_sync(SCHEMA, {"goa-identity": identity}, Secret.COLLECTION_DEFAULT,
f"GOA {provider} credentials for identity {account_id}",
secret, None)
def update(bus, provider, presentation_identity, credentials):
found = find_accounts(bus, provider, presentation_identity)
if not found:
raise LookupError(f"there's no {provider} account for {presentation_identity}")
for path, account in found:
store_credentials(provider, account["Id"], credentials)
try:
for path, _ in found:
ensure_credentials(bus, path)
except GLib.Error as error:
print(f"GNOME Online Accounts: {error.message}", file=sys.stderr)
return 3
return 0
def check(bus, provider, presentation_identity):
found = find(bus, provider, presentation_identity)
if not found:
raise LookupError(f"there's no {provider} account for {presentation_identity}")
for path in found:
ensure_credentials(bus, path)
def remove(bus, provider, presentation_identity):
for path in find(bus, provider, presentation_identity):
call(bus, path, "org.gnome.OnlineAccounts.Account", "Remove", None, "()")
@@ -78,6 +142,11 @@ def main():
return 0
add(bus, account)
return 0
if len(sys.argv) == 4 and sys.argv[1] == "update":
return update(bus, sys.argv[2], sys.argv[3], json.load(sys.stdin))
if len(sys.argv) == 4 and sys.argv[1] == "check":
check(bus, sys.argv[2], sys.argv[3])
return 0
if len(sys.argv) == 4 and sys.argv[1] == "remove":
remove(bus, sys.argv[2], sys.argv[3])
return 0
@@ -91,3 +160,6 @@ if __name__ == "__main__":
except GLib.Error as error:
print(f"GNOME Online Accounts: {error.message}", file=sys.stderr)
sys.exit(1)
except LookupError as error:
print(f"GNOME Online Accounts: {error}", file=sys.stderr)
sys.exit(1)
+15
View File
@@ -31,3 +31,18 @@ nextcloud_revoke_app_password() {
auth_header "$CORE_USER" "$1" | curl -sf -o /dev/null -X DELETE -H @- \
-H 'OCS-APIRequest: true' "$NEXTCLOUD/ocs/v2.php/core/apppassword"
}
# Sign in to Nextcloud as CORE_USER with a password ($1), or PASSWORD if not
# given. Signing in with your Core password (not an app password) also turns
# back on the app passwords Nextcloud turned off when the Core password
# changed. Uses NEXTCLOUD. Returns 2 if Nextcloud says the password is wrong.
nextcloud_sign_in() {
local code
code="$(auth_header "$CORE_USER" "${1:-$PASSWORD}" | curl -s -o /dev/null -w '%{http_code}' -H @- \
-H 'OCS-APIRequest: true' "$NEXTCLOUD/ocs/v2.php/cloud/user?format=json")"
case "$code" in
200) return 0 ;;
401) return 2 ;;
*) return 1 ;;
esac
}