Re-reading python.py showed every mutating endpoint is independently
@admin_required (runs before the shared get_app_or_404 gate), so that gate only
governs the 4 read GETs (packages/env/status/gunicorn-config). Pointing it at
can_access_app safely opens those to grantees while the admin-only mutations stay
untouched — no per-method split needed.
With this, every app-related blueprint is grant-aware. Also fixes the int-vs-str
identity comparison in the helper.
Verified: a python read-gate test (stranger 403, grantee passes), full suite
132 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A grant now reaches every app-related blueprint, not just apps.py + WordPress, so
a shared application is fully manageable by its grantees.
- Promoted the access helpers to a shared seam: ResourceGrantService.can_access_app
(owner/admin/any grant -> reads) and can_edit_app (owner/admin/editor grant ->
mutations); apps.py now delegates to them.
- Swept 33 ownership checks across 6 blueprints (categorized read vs mutate via a
parallel mapping workflow): builds.py (12), domains.py (7, incl. SSL + domain
CRUD), deploy.py (6), private_urls.py (5), templates.py (2), databases.py (1).
Reads -> any grant; mutates -> editor grant.
- Fixed the same latent int-vs-str ownership bug in all 33 (now user.id).
Deferred: python.py's single get_app_or_404 helper gates read+mutate+delete
together and needs per-method splitting before it's safe to make grant-aware.
Verified: cross-blueprint integration test (grantee reaches builds deployments;
stranger 403), full suite 131 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the chosen access-level ACL design: a grant now lets the user actually
operate the resource, gated by a real role.
- Grants carry a role: editor (view + operate) or viewer (read-only). grant POST
validates role in {viewer, editor} (default editor); ResourceGrantService gains
grant_role(); the Sharing UI gains a role selector.
- Two access helpers split the app endpoints: _can_access_app (owner/admin/ANY
grant) gates reads (get_app, /linked, /logs, /container-logs, /containers,
/status); _can_edit_app (owner/admin/EDITOR grant) gates mutations (/link,
/unlink, /environment, PUT, /start, /stop, /restart).
- DELETE stays owner-or-admin (an editor can't delete — tested).
- The same sweep fixed a latent int-vs-str ownership bug across ~14 app endpoints
(compared app.user_id to the stringified get_jwt_identity(); now user.id).
Verified: 3 more ACL tests (viewer read-only + can't operate, editor operates +
can't delete, role validation/default), full suite 130 passed, lint + build.
Still owner-only: env-vars/databases/domains blueprints (follow-up sweep).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An owner/admin can share a specific application (transitively its WordPress
site / DB / domains) with a specific user who doesn't own it. Access-level for
now; per-grant view/edit roles are reserved via a role column.
- New ResourceGrant model (Alembic 016, unique per user x resource) +
ResourceGrantService (grant/revoke/list/user_has_grant/granted_ids).
- Enforcement at the cleanest seams: scope_query gained grant_resource_type so
granted apps appear in the apps list (own OR granted, then workspace-narrowed);
get_app and the WordPress _owner_or_admin_app helper became grant-aware, so one
change opens every WP per-site route to a grantee.
- Grant-management API (GET/POST/DELETE /apps/<id>/grants, owner-or-admin; can't
grant to the owner; revoke is resource-scoped so you can't revoke another app's
grant by id). Uses user.id (int), not the stringified get_jwt_identity().
Verified: 4 new ACL tests (visibility, get_app, WP route access, management perms
+ idempotency + revoke), full suite 127 passed, end-to-end alembic upgrade to 016.
Deferred: a Sharing UI (next), and broadening grant-access to the generic app
operate endpoints (start/stop/update stay owner-or-admin; delete stays owner-only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>