/// Integration Tests: Hierarchical RBAC System /// /// Tests the new fine-grained access control: /// - Role-based access with rules /// - Project/Visibility/Owner scopes /// - Capability checks (HTTP layer) /// - Resource filtering (retrieval layer) use std::sync::Arc; use mem_cli::rbac::{ // Types AccessDecision, AccessRule, AccessScope, Claims, DenyReason, OwnerConstraint, ResourceMeta, ResourceType, Role, Verb, Visibility, // Providers InMemoryRoleProvider, RoleProvider, builtin_role_provider, admin_role, portfolio_agent_role, authenticated_user_role, // Guards AccessGuard, AccessGuardBuilder, InMemoryAuditLogger, }; // ============================================================================ // Role Definition Tests // ============================================================================ #[test] fn test_admin_role_full_access() { let admin = admin_role(); // Admin can do everything assert!(admin.allows(ResourceType::Wiki, Verb::Read)); assert!(admin.allows(ResourceType::Wiki, Verb::Write)); assert!(admin.allows(ResourceType::Wiki, Verb::Delete)); assert!(admin.allows(ResourceType::Conversation, Verb::Delete)); assert!(admin.allows(ResourceType::Embedding, Verb::Query)); } #[test] fn test_portfolio_agent_limited_access() { let agent = portfolio_agent_role(); // Can read/query wiki and embeddings assert!(agent.allows(ResourceType::Wiki, Verb::Read)); assert!(agent.allows(ResourceType::Embedding, Verb::Query)); // Cannot write/delete wiki assert!(!agent.allows(ResourceType::Wiki, Verb::Write)); assert!(!agent.allows(ResourceType::Wiki, Verb::Delete)); // Can write conversations (own only - scope checked separately) assert!(agent.allows(ResourceType::Conversation, Verb::Write)); } #[test] fn test_authenticated_user_private_access() { let user = authenticated_user_role(); // Can read all wiki (including private) assert!(user.allows(ResourceType::Wiki, Verb::Read)); assert!(user.allows(ResourceType::Skill, Verb::Read)); // Can manage own conversations assert!(user.allows(ResourceType::Conversation, Verb::Read)); assert!(user.allows(ResourceType::Conversation, Verb::Write)); assert!(user.allows(ResourceType::Conversation, Verb::Delete)); } // ============================================================================ // Custom Role Tests // ============================================================================ #[test] fn test_custom_role_project_scoped() { let role = Role::new("homelab-reader") .with_rule( AccessRule::new(vec!["wiki", "embedding"], vec![Verb::Read, Verb::Query]) .with_scope(AccessScope::new() .with_projects(vec!["homelab".into()]) .with_visibility(Visibility::Public)) ); // Can read wiki in homelab assert!(role.allows(ResourceType::Wiki, Verb::Read)); // Cannot write assert!(!role.allows(ResourceType::Wiki, Verb::Write)); } #[test] fn test_custom_role_group_scoped() { let role = Role::new("team-internal") .with_rule( AccessRule::new(vec!["wiki"], vec![Verb::Read, Verb::Write]) .with_scope(AccessScope::new() .with_groups(vec!["engineering".into()])) ); let rules = role.rules_for_resource(ResourceType::Wiki); assert_eq!(rules.len(), 1); let scope = &rules[0].scope; assert_eq!(scope.groups, Some(vec!["engineering".into()])); } // ============================================================================ // AccessGuard Capability Tests // ============================================================================ #[tokio::test] async fn test_guard_capability_admin() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let claims = Claims::new("admin-user").with_roles(vec!["admin"]); assert!(guard.check_capability(&claims, Verb::Read).await); assert!(guard.check_capability(&claims, Verb::Write).await); assert!(guard.check_capability(&claims, Verb::Delete).await); assert!(guard.check_capability(&claims, Verb::Query).await); } #[tokio::test] async fn test_guard_capability_portfolio_agent() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let claims = Claims::new("visitor").with_roles(vec!["portfolio-agent"]); assert!(guard.check_capability(&claims, Verb::Read).await); assert!(guard.check_capability(&claims, Verb::Query).await); // Has write for conversations assert!(guard.check_capability(&claims, Verb::Write).await); // No delete assert!(!guard.check_capability(&claims, Verb::Delete).await); } #[tokio::test] async fn test_guard_http_capability_wildcard() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let claims = Claims::new("superuser").with_permissions(vec!["*"]); assert!(guard.check_http_capability(&claims, "memory:read").await); assert!(guard.check_http_capability(&claims, "memory:write").await); assert!(guard.check_http_capability(&claims, "anything:else").await); } #[tokio::test] async fn test_guard_http_capability_direct() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let claims = Claims::new("api-client").with_permissions(vec!["memory:read"]); assert!(guard.check_http_capability(&claims, "memory:read").await); assert!(!guard.check_http_capability(&claims, "memory:write").await); } // ============================================================================ // AccessGuard Resource Tests // ============================================================================ #[tokio::test] async fn test_guard_resource_public_wiki() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let claims = Claims::new("visitor").with_roles(vec!["portfolio-agent"]); let wiki = ResourceMeta::wiki("doc-1", "homelab") .with_visibility(Visibility::Public); assert!(guard.can_read(&claims, &wiki).await); assert!(!guard.can_write(&claims, &wiki).await); } #[tokio::test] async fn test_guard_resource_private_wiki() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let wiki = ResourceMeta::wiki("secret-doc", "homelab") .with_visibility(Visibility::Private); // Portfolio agent cannot read private let visitor = Claims::new("visitor").with_roles(vec!["portfolio-agent"]); assert!(!guard.can_read(&visitor, &wiki).await); // Authenticated user can let user = Claims::new("alice").with_roles(vec!["authenticated-user"]); assert!(guard.can_read(&user, &wiki).await); } #[tokio::test] async fn test_guard_resource_wrong_project() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let claims = Claims::new("visitor").with_roles(vec!["portfolio-agent"]); let wiki = ResourceMeta::wiki("doc-1", "secret-project") .with_visibility(Visibility::Public); // Portfolio agent scope is limited to specific projects assert!(!guard.can_read(&claims, &wiki).await); } #[tokio::test] async fn test_guard_resource_own_conversation() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let claims = Claims::new("visitor-123").with_roles(vec!["portfolio-agent"]); let own_conv = ResourceMeta::conversation("conv-1", "portfolio", "visitor-123"); let other_conv = ResourceMeta::conversation("conv-2", "portfolio", "other-user"); assert!(guard.can_read(&claims, &own_conv).await); assert!(guard.can_write(&claims, &own_conv).await); assert!(!guard.can_read(&claims, &other_conv).await); } // ============================================================================ // Filter Resources Tests // ============================================================================ #[tokio::test] async fn test_guard_filter_by_visibility() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let claims = Claims::new("visitor").with_roles(vec!["portfolio-agent"]); let resources = vec![ ResourceMeta::wiki("public-1", "homelab").with_visibility(Visibility::Public), ResourceMeta::wiki("private-1", "homelab").with_visibility(Visibility::Private), ResourceMeta::wiki("public-2", "portfolio").with_visibility(Visibility::Public), ]; let result = guard.filter_resources(&claims, Verb::Read, resources).await; assert_eq!(result.allowed.len(), 2); assert_eq!(result.denied.len(), 1); let allowed_ids: Vec<_> = result.allowed.iter().map(|r| r.id.as_str()).collect(); assert!(allowed_ids.contains(&"public-1")); assert!(allowed_ids.contains(&"public-2")); } #[tokio::test] async fn test_guard_filter_mixed_resources() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let claims = Claims::new("visitor-123").with_roles(vec!["portfolio-agent"]); let resources = vec![ ResourceMeta::wiki("wiki-1", "homelab").with_visibility(Visibility::Public), ResourceMeta::conversation("conv-own", "portfolio", "visitor-123"), ResourceMeta::conversation("conv-other", "portfolio", "someone-else"), ResourceMeta::skill("skill-1", "homelab"), ]; let result = guard.filter_resources(&claims, Verb::Read, resources).await; // Should allow: wiki-1, conv-own // Should deny: conv-other (not owner), skill-1 (portfolio-agent doesn't have skill access) assert_eq!(result.allowed.len(), 2); } // ============================================================================ // Audit Logging Tests // ============================================================================ #[tokio::test] async fn test_guard_audit_logs_access() { let audit = Arc::new(InMemoryAuditLogger::new()); let guard = AccessGuard::new(Arc::new(builtin_role_provider())) .with_audit(audit.clone()); let claims = Claims::new("test-user").with_roles(vec!["admin"]); let wiki = ResourceMeta::wiki("doc-1", "homelab"); guard.check_access(&claims, &wiki, Verb::Read).await; let entries = audit.entries(); assert_eq!(entries.len(), 1); assert_eq!(entries[0].user_id, "test-user"); assert_eq!(entries[0].decision, "allow"); assert_eq!(entries[0].matched_role, Some("admin".to_string())); } #[tokio::test] async fn test_guard_audit_logs_denial() { let audit = Arc::new(InMemoryAuditLogger::new()); let guard = AccessGuard::new(Arc::new(builtin_role_provider())) .with_audit(audit.clone()); let claims = Claims::new("no-role-user"); let wiki = ResourceMeta::wiki("doc-1", "homelab"); guard.check_access(&claims, &wiki, Verb::Read).await; let entries = audit.entries(); assert_eq!(entries.len(), 1); assert_eq!(entries[0].decision, "deny"); assert!(entries[0].reason.is_some()); } // ============================================================================ // Multiple Roles Tests // ============================================================================ #[tokio::test] async fn test_guard_multiple_roles_union() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); // User with both portfolio-agent and authenticated-user let claims = Claims::new("power-user") .with_roles(vec!["portfolio-agent", "authenticated-user"]); // Private wiki - denied by portfolio-agent, allowed by authenticated-user let private_wiki = ResourceMeta::wiki("secret", "homelab") .with_visibility(Visibility::Private); // Should be allowed (union of permissions) assert!(guard.can_read(&claims, &private_wiki).await); } #[tokio::test] async fn test_guard_no_role_denied() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let claims = Claims::new("anonymous"); // No roles let wiki = ResourceMeta::wiki("public", "homelab") .with_visibility(Visibility::Public); let decision = guard.check_access(&claims, &wiki, Verb::Read).await; assert!(decision.is_denied()); if let AccessDecision::Deny { reason } = decision { assert!(matches!(reason, DenyReason::NoMatchingRole)); } } // ============================================================================ // Builder Tests // ============================================================================ #[tokio::test] async fn test_builder_with_audit() { let audit = Arc::new(InMemoryAuditLogger::new()); let guard = AccessGuardBuilder::new() .with_role_provider(Arc::new(builtin_role_provider())) .with_audit(audit.clone()) .build() .unwrap(); let claims = Claims::new("user").with_roles(vec!["admin"]); guard.check_capability(&claims, Verb::Read).await; assert!(!audit.entries().is_empty()); } #[tokio::test] async fn test_builder_missing_provider() { let result = AccessGuardBuilder::new().build(); assert!(result.is_err()); } // ============================================================================ // Custom Provider Tests // ============================================================================ #[tokio::test] async fn test_custom_role_provider() { let provider = Arc::new( InMemoryRoleProvider::new() .add_role(Role::new("custom-reader") .with_rule(AccessRule::new(vec!["wiki"], vec![Verb::Read]))) ); let guard = AccessGuard::new(provider); let claims = Claims::new("user").with_roles(vec!["custom-reader"]); let wiki = ResourceMeta::wiki("doc", "any-project"); assert!(guard.can_read(&claims, &wiki).await); assert!(!guard.can_write(&claims, &wiki).await); } // ============================================================================ // Real-World Scenario Tests // ============================================================================ #[tokio::test] async fn test_scenario_portfolio_visitor() { // Simulates a visitor to portfolio site let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let visitor = Claims::new("visitor-abc123").with_roles(vec!["portfolio-agent"]); // Can read public docs from allowed projects let public_doc = ResourceMeta::wiki("about-me", "portfolio") .with_visibility(Visibility::Public); assert!(guard.can_read(&visitor, &public_doc).await); // Can query embeddings for RAG let embedding = ResourceMeta::embedding("emb-001", "homelab"); assert!(guard.can_query(&visitor, &embedding).await); // Can chat (own conversations) let conversation = ResourceMeta::conversation("chat-1", "portfolio", "visitor-abc123"); assert!(guard.can_write(&visitor, &conversation).await); // Cannot access private docs let private_doc = ResourceMeta::wiki("secrets", "homelab") .with_visibility(Visibility::Private); assert!(!guard.can_read(&visitor, &private_doc).await); // Cannot access other projects let other_project = ResourceMeta::wiki("internal", "secret-project") .with_visibility(Visibility::Public); assert!(!guard.can_read(&visitor, &other_project).await); } #[tokio::test] async fn test_scenario_authenticated_developer() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let dev = Claims::new("alice") .with_roles(vec!["authenticated-user"]) .with_groups(vec!["engineering"]); // Can read all wiki including private let private_doc = ResourceMeta::wiki("internal-design", "homelab") .with_visibility(Visibility::Private); assert!(guard.can_read(&dev, &private_doc).await); // Can manage own conversations let own_conv = ResourceMeta::conversation("session-1", "homelab", "alice"); assert!(guard.can_read(&dev, &own_conv).await); assert!(guard.can_write(&dev, &own_conv).await); assert!(guard.can_delete(&dev, &own_conv).await); // Cannot delete others' conversations let other_conv = ResourceMeta::conversation("session-2", "homelab", "bob"); assert!(!guard.can_delete(&dev, &other_conv).await); } #[tokio::test] async fn test_scenario_admin_operations() { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); let admin = Claims::new("root").with_roles(vec!["admin"]); // Can do anything let any_resource = ResourceMeta::wiki("anything", "any-project") .with_visibility(Visibility::Private) .with_owner("someone-else"); assert!(guard.can_read(&admin, &any_resource).await); assert!(guard.can_write(&admin, &any_resource).await); assert!(guard.can_delete(&admin, &any_resource).await); // Can manage others' conversations let other_conv = ResourceMeta::conversation("session", "any", "anyone"); assert!(guard.can_delete(&admin, &other_conv).await); }