feat(rbac): complete HTTP endpoint integration + role configs

HTTP Endpoints with RBAC:
- ingest_handler: project-level write access check
- learn_handler: project-level write access check
- projects_handler: filter returned projects by user access
- query_handler: filter search results by resource access
- context_handler: project-level read access check

Example Role Configurations (config/roles/):
- admin.yaml: full access to all resources
- portfolio-agent.yaml: public visitor access
- authenticated-user.yaml: logged-in user access
- homelab-team.yaml: team-scoped project access

All 660+ tests passing.
This commit is contained in:
2026-09-01 08:43:49 -07:00
parent 41cdff3676
commit dae9483a6a
6 changed files with 119 additions and 3 deletions
+50 -1
View File
@@ -441,6 +441,23 @@ pub async fn ingest_handler(
}
let project = body.project.clone();
// RBAC: Check project-level write access
if let Some(guard) = &state.access_guard {
let rbac_claims = to_rbac_claims(&claims);
let project_resource = ResourceMeta::new(&project, ResourceType::Project, &project);
if !guard.can_write(&rbac_claims, &project_resource).await {
tracing::warn!(
"RBAC denied write access to project '{}' for user '{}'",
project, claims.sub
);
return HttpResponse::Forbidden().json(json!({
"error": "forbidden",
"reason": format!("write access denied to project '{}'", project)
}));
}
}
let ingest_id = body.ingest_id.clone();
let records: Vec<(String, String)> = body
.records
@@ -639,6 +656,23 @@ pub async fn learn_handler(
}
let project = body["project"].as_str().unwrap_or("knowledge").to_string();
// RBAC: Check project-level write access
if let Some(guard) = &state.access_guard {
let rbac_claims = to_rbac_claims(&claims);
let project_resource = ResourceMeta::new(&project, ResourceType::Project, &project);
if !guard.can_write(&rbac_claims, &project_resource).await {
tracing::warn!(
"RBAC denied write access to project '{}' for user '{}'",
project, claims.sub
);
return HttpResponse::Forbidden().json(json!({
"error": "forbidden",
"reason": format!("write access denied to project '{}'", project)
}));
}
}
let text = match body["text"].as_str() {
Some(t) => t.to_string(),
None => return HttpResponse::BadRequest().json(json!({
@@ -1017,7 +1051,22 @@ pub async fn projects_handler(
match result {
Ok(rows) => {
let projects: Vec<String> = rows.into_iter().map(|(p,)| p).collect();
let mut projects: Vec<String> = rows.into_iter().map(|(p,)| p).collect();
// RBAC: Filter projects by access
if let Some(guard) = &state.access_guard {
let rbac_claims = to_rbac_claims(&claims);
let mut allowed_projects = Vec::new();
for project in projects {
let resource = ResourceMeta::new(&project, ResourceType::Project, &project);
if guard.can_read(&rbac_claims, &resource).await {
allowed_projects.push(project);
}
}
projects = allowed_projects;
}
HttpResponse::Ok().json(json!({
"projects": projects,
"count": projects.len()