feat(cli): pic groups (tree/create/rename/reparent/rm/members)
Adds the `pic groups` command family over /api/v1/admin/groups: ls/tree, create (--parent), show (path + subgroups + apps), rename (name/description; slug frozen), reparent (--to), rm (--recursive, leaf-first; never auto-deletes apps), and members add/set/rm/ls. Also `pic apps create --group <slug>` to place an app under a group. End-to-end journey tests (tests/groups.rs, DB-gated) cover tree CRUD, delete=RESTRICT (409 on a non-empty group), reparent cycle rejection, and the headline invariant: a group_admin on an ancestor group can deploy to an app it is NOT a direct member of (inherited membership), and loses that access immediately on revoke — all green against a live server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,35 @@ pub struct UserGuard {
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
/// Deletes a group on drop (best-effort). The group must be empty by then
|
||||
/// — register an `AppGuard`/child `GroupGuard` *after* this one so the
|
||||
/// child drops (deletes) first, leaving an empty node here.
|
||||
pub struct GroupGuard {
|
||||
url: String,
|
||||
token: String,
|
||||
slug: String,
|
||||
}
|
||||
|
||||
impl GroupGuard {
|
||||
pub fn new(url: &str, token: &str, slug: &str) -> Self {
|
||||
Self {
|
||||
url: url.to_string(),
|
||||
token: token.to_string(),
|
||||
slug: slug.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GroupGuard {
|
||||
fn drop(&mut self) {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let _ = client
|
||||
.delete(format!("{}/api/v1/admin/groups/{}", self.url, self.slug))
|
||||
.bearer_auth(&self.token)
|
||||
.send();
|
||||
}
|
||||
}
|
||||
|
||||
impl UserGuard {
|
||||
pub fn new(url: &str, token: &str, user_id: &str) -> Self {
|
||||
Self {
|
||||
|
||||
Reference in New Issue
Block a user