Files
claudetools/.claude/skills/owncloud/SKILL.md
Howard Enos da6567ffe0 owncloud: extend skill from read-only audit to full occ management
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>
2026-07-05 20:52:25 -07:00

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 the b2 / seafile fleet 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 -> prints Would: ..., exit 3, no change.
  • trashbin-cleanup / versions-cleanup with no --user and 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-users on those two also hard-fails exit 2 unless --force-all-users is passed in addition to --confirm - a single stray --confirm must 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 -V for the display version).
  • users: occ user:list --show-all-attributes --output=json -> per user: uid, displayName, email, quota, enabled, lastLogin, creationTime, home, backend. lastLogin/creationTime are 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 from config.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 is home::<uid> -> root size in bytes.

What the data means

  • used_bytes is the authoritative per-account storage used; an enabled user with non-zero usage is actively backing up here. usage/audit roll 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 and sysadmin ~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 to du -sb <home>/files for that one user. That du can take minutes on a multi-hundred-GB home (timeout is generous, 900s); used_source in --json reports filecache vs du per 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 former QWM-Sheila); the skill keys off the live occ user:list, so orphans do not appear in users/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), so shares reads straight from the oc_share DB table (joined to oc_filecache for 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 via user: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-users guard).
  • 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 uses ownCloud (not a bare cloud, which would over-match).