feat(manager-core,picloud): per-handler require(capability) checks
Every admin endpoint now resolves Capability for the loaded resource and calls authz::require(...) before mutating. Forbidden → 403; every handler State carries an Arc<dyn AuthzRepo>, plumbed from the new PostgresAppMembersRepository in the picloud binary. * api.rs (scripts): AppRead/AppWriteScript/AppLogRead bound to script.app_id after load. List branches on instance_role: Member → list_for_user, others → list (or ?app= filtered). * apps_api.rs: InstanceCreateApp on POST; AppRead on get/list_domains; AppAdmin on patch/delete/slug:check; AppManageDomains on create_domain/delete_domain. list_apps membership-filters for Member. * admin_users_api.rs: InstanceManageUsers on every endpoint. Mint + PATCH refuse to grant Owner unless the caller is already Owner (CannotEscalate / 422), on top of the existing last-owner guard. * route_admin.rs: AppRead on list/check/match; AppWriteRoute on create/delete bound to the route's actual app_id (added a RouteRepository::get(uuid) lookup so delete binds correctly). * AppRepository + ScriptRepository gain list_for_user(user_id) for membership-filtered listings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,10 @@ pub struct NewRoute {
|
||||
#[async_trait]
|
||||
pub trait RouteRepository: Send + Sync {
|
||||
async fn list_all(&self) -> Result<Vec<Route>, ScriptRepositoryError>;
|
||||
/// Single-row lookup. Used by `DELETE /api/v1/admin/routes/{id}` so
|
||||
/// the capability check binds to the route's actual `app_id`
|
||||
/// (not a path param).
|
||||
async fn get(&self, route_id: Uuid) -> Result<Option<Route>, ScriptRepositoryError>;
|
||||
async fn list_for_app(&self, app_id: AppId) -> Result<Vec<Route>, ScriptRepositoryError>;
|
||||
async fn list_for_script(
|
||||
&self,
|
||||
@@ -66,6 +70,18 @@ impl RouteRepository for PostgresRouteRepository {
|
||||
Ok(rows.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
async fn get(&self, route_id: Uuid) -> Result<Option<Route>, ScriptRepositoryError> {
|
||||
let row = sqlx::query_as::<_, RouteRow>(
|
||||
"SELECT id, app_id, script_id, host_kind, host, host_param_name, \
|
||||
path_kind, path, method, created_at \
|
||||
FROM routes WHERE id = $1",
|
||||
)
|
||||
.bind(route_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
Ok(row.map(Into::into))
|
||||
}
|
||||
|
||||
async fn list_for_app(&self, app_id: AppId) -> Result<Vec<Route>, ScriptRepositoryError> {
|
||||
let rows = sqlx::query_as::<_, RouteRow>(
|
||||
"SELECT id, app_id, script_id, host_kind, host, host_param_name, \
|
||||
|
||||
Reference in New Issue
Block a user