Server-side foundation for Phase-2 groups (no group-owned resources yet):
Shared types:
- GroupId, Group; App gains group_id; AppRole::{precedence,max} for
folding the highest effective role across the membership chain.
Repos:
- group_repo: tree CRUD with reparent (ancestor-walk cycle guard under a
coarse instance-wide structural advisory lock; slug frozen; bumps
structure_version) and delete=RESTRICT (refuses non-empty groups).
- group_members_repo: per-(user, group) role grants, mirroring app_members.
Hierarchy-aware authz (§5.3):
- AuthzRepo gains effective_app_role / effective_group_role (default to
direct membership / none, so the ~18 existing test stubs are untouched);
the Postgres impl resolves each via one depth-bounded recursive CTE that
MAXes the app's own row with every ancestor group_members row.
- can(): the Member path now folds inherited group roles, so a group_admin
on any ancestor is implicitly app_admin beneath it. New Capability
variants InstanceCreateGroup / Group{Read,Write,Admin}; group caps carry
no app_id (bound API keys can't manage groups). 8 new unit tests.
Admin API:
- groups_api: group CRUD + reparent (admin at both source and destination
parent, §5.6) + per-group members, all capability-gated.
- apps: POST /apps takes an optional parent group (default root); app
responses carry group_id; my_role now reflects the effective role.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
61 lines
1.3 KiB
Rust
61 lines
1.3 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
macro_rules! id_type {
|
|
($name:ident) => {
|
|
#[derive(
|
|
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
|
|
)]
|
|
#[serde(transparent)]
|
|
pub struct $name(pub Uuid);
|
|
|
|
impl $name {
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self(Uuid::new_v4())
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn into_inner(self) -> Uuid {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl Default for $name {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl From<Uuid> for $name {
|
|
fn from(u: Uuid) -> Self {
|
|
Self(u)
|
|
}
|
|
}
|
|
|
|
impl From<$name> for Uuid {
|
|
fn from(id: $name) -> Self {
|
|
id.0
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for $name {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.0.fmt(f)
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
id_type!(ScriptId);
|
|
id_type!(ExecutionId);
|
|
id_type!(RequestId);
|
|
id_type!(AdminUserId);
|
|
id_type!(AppId);
|
|
id_type!(ApiKeyId);
|
|
id_type!(TriggerId);
|
|
id_type!(AppUserId);
|
|
id_type!(InvitationId);
|
|
id_type!(QueueMessageId);
|
|
id_type!(GroupId);
|