Phase 1: Wiki-Link Graph Indexing - WikiLinkParser: extract [[links]] from markdown - WikiLinkGraph: BFS traversal, reachable docs, backlinks - Support relative path resolution (../../../) Phase 2: ScoringPipeline trait (SOLID design) - DocumentScorer trait: single interface for all scorers - GlobalTfIdfScorer, ProjectTfIdfScorer, SemanticScorer - MetadataBoostingScorer (decorator pattern) - ScoringPipeline: orchestrate multiple scorers with RRF fusion - Benefits: add new scorers without modifying existing code Phase 7: RBAC + PolicyProvider trait - PolicyProvider trait: pluggable backends (Vault, Postgres, Redis) - VaultPolicyProvider: load YAML from vault/projects/* and vault/shared/skills/* - MockPolicyProvider: for testing (no I/O) - AccessChecker trait: single-purpose RBAC checks - AccessLevelChecker, RoleChecker, PermissionChecker - AccessDecisionEngine: orchestrate checkers with short-circuit eval - AuditLogger trait: pluggable audit backends Test Fixtures (DRY principle) - OidcClaimsBuilder: fluent API for test data - AccessPolicyBuilder: fluent API for policies - MockPolicyProvider, MockAuditLogger: testing mocks All modules compile and unit tests pass.
186 lines
5.0 KiB
Rust
186 lines
5.0 KiB
Rust
/// Fluent builders for test data construction (DRY principle)
|
|
|
|
/// Builder for OidcClaims (OIDC token claims from Authentik)
|
|
pub struct OidcClaimsBuilder {
|
|
sub: String,
|
|
groups: Vec<String>,
|
|
roles: Vec<String>,
|
|
permissions: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct OidcClaims {
|
|
pub sub: String,
|
|
pub groups: Vec<String>,
|
|
pub roles: Vec<String>,
|
|
pub permissions: Vec<String>,
|
|
}
|
|
|
|
impl OidcClaimsBuilder {
|
|
pub fn new(sub: &str) -> Self {
|
|
Self {
|
|
sub: sub.to_string(),
|
|
groups: vec![],
|
|
roles: vec!["viewer".to_string()],
|
|
permissions: vec![],
|
|
}
|
|
}
|
|
|
|
pub fn group(mut self, group: &str) -> Self {
|
|
self.groups.push(group.to_string());
|
|
self
|
|
}
|
|
|
|
pub fn groups(mut self, groups: Vec<&str>) -> Self {
|
|
self.groups = groups.iter().map(|s| s.to_string()).collect();
|
|
self
|
|
}
|
|
|
|
pub fn role(mut self, role: &str) -> Self {
|
|
self.roles.push(role.to_string());
|
|
self
|
|
}
|
|
|
|
pub fn permission(mut self, perm: &str) -> Self {
|
|
self.permissions.push(perm.to_string());
|
|
self
|
|
}
|
|
|
|
pub fn permissions(mut self, perms: Vec<&str>) -> Self {
|
|
self.permissions = perms.iter().map(|s| s.to_string()).collect();
|
|
self
|
|
}
|
|
|
|
pub fn build(self) -> OidcClaims {
|
|
OidcClaims {
|
|
sub: self.sub,
|
|
groups: self.groups,
|
|
roles: self.roles,
|
|
permissions: self.permissions,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Builder for AccessPolicy (RBAC policy from Vault)
|
|
pub struct AccessPolicyBuilder {
|
|
access_level: String,
|
|
owner_group: String,
|
|
allowed_groups: Vec<String>,
|
|
required_role: Option<String>,
|
|
required_permission: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct AccessPolicy {
|
|
pub access_level: String,
|
|
pub owner_group: String,
|
|
pub allowed_groups: Vec<String>,
|
|
pub required_role: Option<String>,
|
|
pub required_permission: Option<String>,
|
|
}
|
|
|
|
impl AccessPolicyBuilder {
|
|
pub fn public() -> Self {
|
|
Self {
|
|
access_level: "public".to_string(),
|
|
owner_group: Default::default(),
|
|
allowed_groups: Default::default(),
|
|
required_role: None,
|
|
required_permission: None,
|
|
}
|
|
}
|
|
|
|
pub fn private(mut self, owner: &str) -> Self {
|
|
self.access_level = "private".to_string();
|
|
self.owner_group = owner.to_string();
|
|
self
|
|
}
|
|
|
|
pub fn group(mut self, groups: Vec<&str>) -> Self {
|
|
self.access_level = "group".to_string();
|
|
self.allowed_groups = groups.iter().map(|s| s.to_string()).collect();
|
|
self
|
|
}
|
|
|
|
pub fn require_role(mut self, role: &str) -> Self {
|
|
self.required_role = Some(role.to_string());
|
|
self
|
|
}
|
|
|
|
pub fn require_permission(mut self, perm: &str) -> Self {
|
|
self.required_permission = Some(perm.to_string());
|
|
self
|
|
}
|
|
|
|
pub fn build(self) -> AccessPolicy {
|
|
AccessPolicy {
|
|
access_level: self.access_level,
|
|
owner_group: self.owner_group,
|
|
allowed_groups: self.allowed_groups,
|
|
required_role: self.required_role,
|
|
required_permission: self.required_permission,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_oidc_claims_builder_basic() {
|
|
let claims = OidcClaimsBuilder::new("charlie")
|
|
.group("platform-team")
|
|
.permission("memory:read")
|
|
.build();
|
|
|
|
assert_eq!(claims.sub, "charlie");
|
|
assert_eq!(claims.groups, vec!["platform-team"]);
|
|
assert!(claims.permissions.contains(&"memory:read".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_oidc_claims_builder_multiple_groups() {
|
|
let claims = OidcClaimsBuilder::new("alice")
|
|
.groups(vec!["platform-team", "devops-team"])
|
|
.role("editor")
|
|
.build();
|
|
|
|
assert_eq!(claims.groups.len(), 2);
|
|
assert!(claims.groups.contains(&"platform-team".to_string()));
|
|
assert!(claims.groups.contains(&"devops-team".to_string()));
|
|
assert!(claims.roles.contains(&"editor".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_access_policy_public() {
|
|
let policy = AccessPolicyBuilder::public().build();
|
|
|
|
assert_eq!(policy.access_level, "public");
|
|
assert!(policy.owner_group.is_empty());
|
|
assert!(policy.allowed_groups.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_access_policy_private() {
|
|
let policy = AccessPolicyBuilder::public()
|
|
.private("ml-team")
|
|
.build();
|
|
|
|
assert_eq!(policy.access_level, "private");
|
|
assert_eq!(policy.owner_group, "ml-team");
|
|
}
|
|
|
|
#[test]
|
|
fn test_access_policy_group() {
|
|
let policy = AccessPolicyBuilder::public()
|
|
.group(vec!["platform-team", "devops-team"])
|
|
.require_role("viewer")
|
|
.build();
|
|
|
|
assert_eq!(policy.access_level, "group");
|
|
assert_eq!(policy.allowed_groups.len(), 2);
|
|
assert_eq!(policy.required_role, Some("viewer".to_string()));
|
|
}
|
|
}
|