Layer gated admin writes onto the read-only GPS-audit skill, driven through occ over SSH (same paramiko/vault/filecache plumbing): - user lifecycle (add/delete/enable/disable/reset-password/modify), quota get/set/reset, groups (create/delete/members), read-only share listing (from oc_share; occ has no share create/list), apps, system config, maintenance mode, files scan, ownership transfer, trashbin/versions cleanup. - All writes gated behind --confirm (refuse -> rc 3); hard-fail guards (rc 2) for invalid uid, missing password, and all-users cleanup with no target. - --all-users trashbin/versions purge now ALSO requires --force-all-users in addition to --confirm, so a single stray --confirm cannot trigger an irreversible fleet-wide purge. Built against the verified live occ surface (occ list / occ help), not guessed flags. Fixes from code-review + security-review: Python-side share filtering (no MySQL SQL-injection via '' -doubling), occ reads no longer mask a failed read as healthy (maintenance/quota/config), guarded json.loads, clean --json output, and '--' end-of-options guards so dash-leading uids can't be parsed as occ flags. dry_test.sh added as a non-destructive regression harness (43 checks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.1 KiB
name, description
| name | description |
|---|---|
| owncloud | Manage ACG's ownCloud server (cloud.acghosting.com / 172.16.3.22, ownCloud 10.16 Community) via occ over SSH: read-only GPS backup inventory (who has an account, per-user storage + quota + last login, which GuruRMM endpoints run the desktop client) PLUS gated admin writes - user lifecycle, quota, groups, apps/config, maintenance mode, files scan/transfer, trashbin/versions cleanup. Reads run freely; writes require --confirm. Triggers: owncloud, cloud.acghosting, who backs up to owncloud, owncloud usage, owncloud audit, create/disable owncloud user, set owncloud quota, owncloud group, owncloud maintenance mode. |
ownCloud Skill
Client for ACG's live ownCloud 10.16.0 Community server
(cloud.acghosting.com, VM 172.16.3.22, Rocky Linux 9.6), driven entirely
through the occ admin CLI over SSH.
Two halves:
- Read (GPS audit, ungated): which customers back up here and how much data
they hold - and, with
--rmm, which enrolled machines actually run the ownCloud desktop client. - Manage (gated): user/quota/group lifecycle, apps/config, maintenance mode,
filesystem scans, ownership transfer, trashbin/versions cleanup. Every
state-changing command refuses without
--confirm(prints the WOULD line, exits 3), mirroring theb2/seafilefleet convention.
This skill is one of the backup-verification siblings alongside seafile,
b2 (Backblaze) and datto-workplace.
Running - read (GPS audit)
OWNCLOUD=".claude/skills/owncloud/scripts/owncloud.py"
py "$OWNCLOUD" status # SSH ok + occ version / edition
py "$OWNCLOUD" users [--json] # all users + enabled/quota/used/last login
py "$OWNCLOUD" usage [--json] # per-user rollup, used GB desc (the audit view)
py "$OWNCLOUD" audit [--client NAME] [--rmm] [--json]
Add --json to users / usage / audit for machine-readable output.
--host overrides the server IP (default 172.16.3.22).
Running - manage (writes gated behind --confirm)
# users
py "$OWNCLOUD" user-add jdoe --display-name "J Doe" --email j@x.com -g Clients --gen-password --confirm
py "$OWNCLOUD" user-disable jdoe --confirm # user-enable to re-enable
py "$OWNCLOUD" user-reset-password jdoe --password-env OC_NEW_PW --confirm
py "$OWNCLOUD" user-modify jdoe --email new@x.com --confirm
py "$OWNCLOUD" user-delete jdoe --confirm # DELETES the user AND all their data
# quota (quota <uid> reads it; value like '5 GB', 'none' = unlimited)
py "$OWNCLOUD" quota jdoe
py "$OWNCLOUD" quota-set jdoe "50 GB" --confirm # quota-reset -> back to default
# groups (groups / group-members / user-groups are read-only)
py "$OWNCLOUD" groups
py "$OWNCLOUD" group-add Clients --confirm
py "$OWNCLOUD" group-add-member Clients -m jdoe -m asmith --confirm
py "$OWNCLOUD" group-remove-member Clients -m jdoe --confirm
py "$OWNCLOUD" group-delete Clients --confirm
# shares (read-only - occ cannot create shares; see Notes)
py "$OWNCLOUD" shares [--user jdoe]
# server / apps / config
py "$OWNCLOUD" maintenance status|on|off [--confirm] # on/off need --confirm
py "$OWNCLOUD" apps
py "$OWNCLOUD" app-enable|app-disable <app> --confirm
py "$OWNCLOUD" config-get [name] # no name -> dump all system config
py "$OWNCLOUD" config-set <name> --value V [--type string|integer|double|boolean|json] --confirm
# files admin
py "$OWNCLOUD" files-scan (--user jdoe | --all) [--path sub/dir] --confirm
py "$OWNCLOUD" transfer-ownership <src> <dst> [--path folder] --confirm
py "$OWNCLOUD" trashbin-cleanup --user jdoe --confirm
py "$OWNCLOUD" versions-cleanup --user jdoe --confirm
# fleet-wide purge needs the extra ack flag on top of --confirm:
py "$OWNCLOUD" versions-cleanup --all-users --force-all-users --confirm
Passwords + the credential rule
user-add / user-reset-password take the password one of three ways:
--gen-password (generates a strong one, prints it once), --password-env VAR
(reads it from that env var - preferred, keeps it out of shell history), or a
literal --password (discouraged: lands in history + ps). On the server the
value is passed via occ's own OC_PASS + --password-from-env.
Any password created or reset here MUST be stored in the SOPS vault (house
rule) - the command prints a [CRITICAL] reminder with the exact vault.sh
line (suggested path infrastructure/owncloud-users/<uid>.sops.yaml).
Gating / exit codes
- write command without
--confirm-> printsWould: ..., exit 3, no change. trashbin-cleanup/versions-cleanupwith no--userand no--all-users-> hard-fail exit 2 (occ treats "no user" as every user; a fleet-wide purge must be made explicit with--all-users).--all-userson those two also hard-fails exit 2 unless--force-all-usersis passed in addition to--confirm- a single stray--confirmmust never be enough to irreversibly purge every user's history.- invalid uid on
user-add-> exit 2.
Access channel
There is no ownCloud admin web / OCS account in the vault, so the channel is
SSH + the occ CLI, not the OCS HTTP API (the OCS endpoint is reachable at
https://cloud.acghosting.com/status.php but we lack an OCS admin cred). The
client SSHes to root@172.16.3.22:22 with paramiko (password auth,
AutoAddPolicy) and runs occ as the web user:
sudo -u apache php /var/www/owncloud/occ <cmd>
Credentials load lazily from the SOPS vault (never hardcoded):
infrastructure/owncloud-vm.sops.yaml -> credentials.username (root) /
credentials.password.
The occ / DB commands it settled on
- status:
occ status --output=json(+occ -Vfor the display version). - users:
occ user:list --show-all-attributes --output=json-> per user:uid, displayName, email, quota, enabled, lastLogin, creationTime, home, backend.lastLogin/creationTimeare Unix epoch seconds (0 = never); the client renders them as UTC. - used bytes: occ has no
user:info/ per-user usage on this version, so used storage is read from ownCloud's own filecache (the exact figure the UI shows). DB creds are read live fromconfig.php(we already have root), then:SELECT s.id, fc.size FROM oc_storages s JOIN oc_filecache fc ON fc.storage=s.numeric_id WHERE s.id LIKE 'home::%' AND fc.path='';Each row ishome::<uid>-> root size in bytes.
What the data means
- used_bytes is the authoritative per-account storage used; an
enableduser with non-zero usage is actively backing up here.usage/auditroll this up, sorted desc, with a total. - quota is a string:
default,none(unlimited), or a byte/size value. - Usage is concentrated: as of the build (2026-07-05) a single account (
pavon) holds ~14 TB andsysadmin~2.9 TB, dwarfing the rest - ownCloud is used by a handful of clients, not the whole GPS base.
The "both" cross-check (--rmm)
Server-side (occ) tells you who has an account; --rmm adds the endpoint half. It
logs in to GuruRMM (infrastructure/gururmm-server.sops.yaml) and, for each agent
(filter with --client), runs a read-only detection PowerShell reporting
installed products / running processes / config folders matching ownCloud.
An agent is reported only if the client is actually present. This dispatches a
command to live endpoints - always scope with --client rather than sweeping
all 350+ agents.
Notes / gotchas / limitations
- Dirty filecache (-1): if a user's cached root size is
-1(cache not yet computed), the client falls back todu -sb <home>/filesfor that one user. That du can take minutes on a multi-hundred-GB home (timeout is generous, 900s);used_sourcein--jsonreportsfilecachevsduper user. At build time one account (anaise) was dirty and resolved via du (~0.6 TB). - Orphan storages: the filecache can contain
home::<uid>rows for deleted users (e.g. a formerQWM-Sheila); the skill keys off the liveocc user:list, so orphans do not appear inusers/usage. - No OCS/web API: deeper per-file / sharing queries would need an OCS admin cred we do not have; occ + filecache cover the audit needs.
- Shares are read-only here: occ on this version exposes no share create/list
(only
sharing:cleanup-remote-storages), sosharesreads straight from theoc_shareDB table (joined tooc_filecachefor the real path). Creating a share would need the OCS API / an admin web cred we do not have. - occ command surface was verified live against this exact 10.16 box (
occ list/occ help <cmd>) before wiring the writes - flags differ by version. Notable ones used:user:modify <uid> displayname|email, quota viauser:setting <uid> files quota --value=, passwords via--password-from-env,files:transfer-ownership -s,trashbin:cleanup/versions:cleanup(no user arg = ALL users, hence the--all-usersguard). - Shared plumbing (repo-root resolve, vault read, GuruRMM client + endpoint
detection) lives in
.claude/scripts/backup_common.py, used by all the server-backup skills. Detection patterns must be precise per backend - this skill usesownCloud(not a barecloud, which would over-match).