- Remove gitlinks (160000 mode) for chainfire, flaredb, iam - Add workspace contents as regular tracked files - Update flake.nix to use simple paths instead of builtins.fetchGit This resolves the nix build failure where submodule directories appeared empty in the nix store. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
221 lines
6.6 KiB
Rust
221 lines
6.6 KiB
Rust
//! Group membership storage
|
|
//!
|
|
//! Tracks which principals are members of which groups.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use iam_types::{Error, PrincipalKind, PrincipalRef, Result};
|
|
|
|
use crate::backend::{Backend, CasResult, JsonStore, KvPair, StorageBackend};
|
|
|
|
/// Key prefixes for group membership storage
|
|
mod keys {
|
|
/// Members of a group
|
|
/// Format: iam/groups/{group_id}/members/{kind}/{member_id}
|
|
pub const GROUP_MEMBERS: &str = "iam/groups/";
|
|
|
|
/// Groups a principal belongs to (reverse index)
|
|
/// Format: iam/memberships/{kind}/{principal_id}/{group_id}
|
|
pub const PRINCIPAL_GROUPS: &str = "iam/memberships/";
|
|
}
|
|
|
|
/// Store for group memberships
|
|
pub struct GroupStore {
|
|
backend: Arc<Backend>,
|
|
}
|
|
|
|
impl JsonStore for GroupStore {
|
|
fn backend(&self) -> &Backend {
|
|
&self.backend
|
|
}
|
|
}
|
|
|
|
impl GroupStore {
|
|
/// Create a new group store
|
|
pub fn new(backend: Arc<Backend>) -> Self {
|
|
Self { backend }
|
|
}
|
|
|
|
/// Add a member to a group
|
|
pub async fn add_member(&self, group_id: &str, member: &PrincipalRef) -> Result<()> {
|
|
// Create forward index (group -> member)
|
|
let forward_key = format!(
|
|
"{}{}/members/{}/{}",
|
|
keys::GROUP_MEMBERS,
|
|
group_id,
|
|
member.kind,
|
|
member.id
|
|
);
|
|
|
|
// Create reverse index (member -> group)
|
|
let reverse_key = format!(
|
|
"{}{}/{}/{}",
|
|
keys::PRINCIPAL_GROUPS,
|
|
member.kind,
|
|
member.id,
|
|
group_id
|
|
);
|
|
|
|
// Store membership marker (empty value, just presence matters)
|
|
let marker = b"1";
|
|
|
|
// Create forward index
|
|
match self.backend.cas(forward_key.as_bytes(), 0, marker).await? {
|
|
CasResult::Success(_) => {}
|
|
CasResult::Conflict { .. } => {
|
|
// Already a member, that's fine
|
|
return Ok(());
|
|
}
|
|
CasResult::NotFound => return Err(Error::Internal("Unexpected CAS result".into())),
|
|
}
|
|
|
|
// Create reverse index
|
|
self.backend
|
|
.cas(reverse_key.as_bytes(), 0, marker)
|
|
.await
|
|
.ok();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove a member from a group
|
|
pub async fn remove_member(&self, group_id: &str, member: &PrincipalRef) -> Result<bool> {
|
|
let forward_key = format!(
|
|
"{}{}/members/{}/{}",
|
|
keys::GROUP_MEMBERS,
|
|
group_id,
|
|
member.kind,
|
|
member.id
|
|
);
|
|
let reverse_key = format!(
|
|
"{}{}/{}/{}",
|
|
keys::PRINCIPAL_GROUPS,
|
|
member.kind,
|
|
member.id,
|
|
group_id
|
|
);
|
|
|
|
// Delete forward index
|
|
let deleted = self.backend.delete(forward_key.as_bytes()).await?;
|
|
|
|
// Delete reverse index
|
|
self.backend.delete(reverse_key.as_bytes()).await.ok();
|
|
|
|
Ok(deleted)
|
|
}
|
|
|
|
/// List all members of a group
|
|
pub async fn list_members(&self, group_id: &str) -> Result<Vec<PrincipalRef>> {
|
|
let prefix = format!("{}{}/members/", keys::GROUP_MEMBERS, group_id);
|
|
let pairs = self.backend.scan_prefix(prefix.as_bytes(), 1000).await?;
|
|
|
|
let mut members = Vec::new();
|
|
for KvPair { key, .. } in pairs {
|
|
// Key format: iam/groups/{group_id}/members/{kind}/{member_id}
|
|
let key_str = String::from_utf8_lossy(&key);
|
|
let parts: Vec<&str> = key_str.split('/').collect();
|
|
if parts.len() >= 6 {
|
|
let kind_str = parts[4];
|
|
let member_id = parts[5];
|
|
|
|
let kind = match kind_str {
|
|
"user" => PrincipalKind::User,
|
|
"service_account" => PrincipalKind::ServiceAccount,
|
|
"group" => PrincipalKind::Group,
|
|
_ => continue,
|
|
};
|
|
|
|
members.push(PrincipalRef::new(kind, member_id));
|
|
}
|
|
}
|
|
|
|
Ok(members)
|
|
}
|
|
|
|
/// List all groups a principal belongs to
|
|
pub async fn list_groups(&self, principal: &PrincipalRef) -> Result<Vec<String>> {
|
|
let prefix = format!(
|
|
"{}{}/{}/",
|
|
keys::PRINCIPAL_GROUPS,
|
|
principal.kind,
|
|
principal.id
|
|
);
|
|
let pairs = self.backend.scan_prefix(prefix.as_bytes(), 1000).await?;
|
|
|
|
let mut groups = Vec::new();
|
|
for KvPair { key, .. } in pairs {
|
|
// Key format: iam/memberships/{kind}/{principal_id}/{group_id}
|
|
let key_str = String::from_utf8_lossy(&key);
|
|
let parts: Vec<&str> = key_str.split('/').collect();
|
|
if parts.len() >= 5 {
|
|
groups.push(parts[4].to_string());
|
|
}
|
|
}
|
|
|
|
Ok(groups)
|
|
}
|
|
|
|
/// Check if a principal is a member of a group
|
|
pub async fn is_member(&self, group_id: &str, member: &PrincipalRef) -> Result<bool> {
|
|
let key = format!(
|
|
"{}{}/members/{}/{}",
|
|
keys::GROUP_MEMBERS,
|
|
group_id,
|
|
member.kind,
|
|
member.id
|
|
);
|
|
let result = self.backend.get(key.as_bytes()).await?;
|
|
Ok(result.is_some())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::Backend;
|
|
|
|
#[tokio::test]
|
|
async fn test_add_and_list_members() {
|
|
let backend = Arc::new(Backend::memory());
|
|
let store = GroupStore::new(backend);
|
|
|
|
let alice = PrincipalRef::user("alice");
|
|
let bob = PrincipalRef::user("bob");
|
|
|
|
store.add_member("devs", &alice).await.unwrap();
|
|
store.add_member("devs", &bob).await.unwrap();
|
|
|
|
let members = store.list_members("devs").await.unwrap();
|
|
assert_eq!(members.len(), 2);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_remove_member() {
|
|
let backend = Arc::new(Backend::memory());
|
|
let store = GroupStore::new(backend);
|
|
|
|
let alice = PrincipalRef::user("alice");
|
|
|
|
store.add_member("devs", &alice).await.unwrap();
|
|
assert!(store.is_member("devs", &alice).await.unwrap());
|
|
|
|
store.remove_member("devs", &alice).await.unwrap();
|
|
assert!(!store.is_member("devs", &alice).await.unwrap());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_list_groups_for_principal() {
|
|
let backend = Arc::new(Backend::memory());
|
|
let store = GroupStore::new(backend);
|
|
|
|
let alice = PrincipalRef::user("alice");
|
|
|
|
store.add_member("devs", &alice).await.unwrap();
|
|
store.add_member("admins", &alice).await.unwrap();
|
|
|
|
let groups = store.list_groups(&alice).await.unwrap();
|
|
assert_eq!(groups.len(), 2);
|
|
assert!(groups.contains(&"devs".to_string()));
|
|
assert!(groups.contains(&"admins".to_string()));
|
|
}
|
|
}
|