Archived
153 lines
5.5 KiB
Rust
153 lines
5.5 KiB
Rust
//! AuthGuard — the mechanism behind `@authenticate`.
|
|
//!
|
|
//! `AuthGuard` is the bridge between a raw session/JWT token string (extracted
|
|
//! from the request) and a fully-resolved `IdentityContext`.
|
|
//!
|
|
//! Execution:
|
|
//! 1. Extract session ID from the token (JWT decode or opaque lookup)
|
|
//! 2. Validate the Session node in Engram (expiry check)
|
|
//! 3. Load the User node
|
|
//! 4. Traverse `User ──has_role──▶ Role` edges
|
|
//! 5. Traverse `Role ──grants──▶ Scope` edges
|
|
//! 6. Return `IdentityContext` — fully resolved, ready for downstream use
|
|
//!
|
|
//! `@public` bypasses this guard entirely (see `el-aop::PublicMarker`).
|
|
|
|
use crate::{
|
|
context::IdentityContext,
|
|
engram::EngramClient,
|
|
error::{IdentityError, IdentityResult},
|
|
nodes::{Role, Scope, User, EDGE_GRANTS, EDGE_HAS_ROLE, NODE_ROLE, NODE_SCOPE, NODE_USER},
|
|
session::SessionManager,
|
|
};
|
|
use std::sync::Arc;
|
|
|
|
/// AuthGuard resolves a session/token string into a full `IdentityContext`.
|
|
///
|
|
/// Configured once at application startup and shared across requests.
|
|
pub struct AuthGuard {
|
|
client: Arc<dyn EngramClient>,
|
|
session_manager: Arc<SessionManager>,
|
|
}
|
|
|
|
impl AuthGuard {
|
|
pub fn new(client: Arc<dyn EngramClient>, session_manager: Arc<SessionManager>) -> Self {
|
|
Self { client, session_manager }
|
|
}
|
|
|
|
/// Authenticate a request given its session ID.
|
|
///
|
|
/// This is called by `@authenticate` in the AOP chain. For every protected
|
|
/// endpoint, this runs before the handler. If it returns `Err`, the request
|
|
/// is rejected.
|
|
pub fn authenticate(&self, session_id: &str) -> IdentityResult<IdentityContext> {
|
|
// 1. Validate session (checks expiry, lazy-deletes expired)
|
|
let session = self.session_manager.validate(session_id)?;
|
|
|
|
// 2. Load User node
|
|
let user_id_str = session.user_id.to_string();
|
|
let user_node = self
|
|
.client
|
|
.get_node(&user_id_str)
|
|
.map_err(|e| IdentityError::GraphError(e.to_string()))?
|
|
.ok_or_else(|| IdentityError::UserNotFound(user_id_str.clone()))?;
|
|
|
|
let user = User::from_value(&user_node)
|
|
.ok_or_else(|| IdentityError::GraphError("user node parse failed".into()))?;
|
|
|
|
// 3. Load roles via has_role edges
|
|
let role_nodes = self
|
|
.client
|
|
.find_connected(&user_id_str, EDGE_HAS_ROLE)
|
|
.map_err(|e| IdentityError::GraphError(e.to_string()))?;
|
|
|
|
let roles: Vec<Role> = role_nodes
|
|
.iter()
|
|
.filter_map(Role::from_value)
|
|
.collect();
|
|
|
|
// 4. Load scopes via grants edges from each role
|
|
let mut scopes: Vec<Scope> = Vec::new();
|
|
for role in &roles {
|
|
let scope_nodes = self
|
|
.client
|
|
.find_connected(&role.id.to_string(), EDGE_GRANTS)
|
|
.map_err(|e| IdentityError::GraphError(e.to_string()))?;
|
|
scopes.extend(scope_nodes.iter().filter_map(Scope::from_value));
|
|
}
|
|
|
|
// Deduplicate scopes by name
|
|
scopes.dedup_by(|a, b| a.name == b.name);
|
|
|
|
Ok(IdentityContext::new(user, session, roles, scopes))
|
|
}
|
|
|
|
/// Require a specific role — returns Err::Forbidden if missing.
|
|
pub fn require_role(
|
|
&self,
|
|
ctx: &IdentityContext,
|
|
role: &str,
|
|
) -> IdentityResult<()> {
|
|
if ctx.has_role(role) {
|
|
Ok(())
|
|
} else {
|
|
Err(IdentityError::Forbidden(role.to_string()))
|
|
}
|
|
}
|
|
|
|
/// Require a specific scope — returns Err::ScopeForbidden if missing.
|
|
pub fn require_scope(
|
|
&self,
|
|
ctx: &IdentityContext,
|
|
scope: &str,
|
|
) -> IdentityResult<()> {
|
|
if ctx.has_scope(scope) {
|
|
Ok(())
|
|
} else {
|
|
Err(IdentityError::ScopeForbidden(scope.to_string()))
|
|
}
|
|
}
|
|
|
|
/// Register a user in the Engram graph.
|
|
///
|
|
/// Creates the User node. Call this after first OAuth login or on signup.
|
|
pub fn register_user(&self, user: &User) -> IdentityResult<()> {
|
|
self.client
|
|
.create_node(NODE_USER, user.to_value())
|
|
.map(|_| ())
|
|
.map_err(|e| IdentityError::GraphError(e.to_string()))
|
|
}
|
|
|
|
/// Assign a role to a user by creating a `has_role` edge.
|
|
pub fn assign_role(&self, user_id: &str, role: &Role) -> IdentityResult<()> {
|
|
// Ensure role node exists
|
|
if self.client.get_node(&role.id.to_string())
|
|
.map_err(|e| IdentityError::GraphError(e.to_string()))?
|
|
.is_none()
|
|
{
|
|
self.client
|
|
.create_node(NODE_ROLE, role.to_value())
|
|
.map_err(|e| IdentityError::GraphError(e.to_string()))?;
|
|
}
|
|
// Edge: User → Role
|
|
self.client
|
|
.create_edge(user_id, &role.id.to_string(), EDGE_HAS_ROLE)
|
|
.map_err(|e| IdentityError::GraphError(e.to_string()))
|
|
}
|
|
|
|
/// Register a scope and link it to a role via a `grants` edge.
|
|
pub fn assign_scope_to_role(&self, role: &Role, scope: &Scope) -> IdentityResult<()> {
|
|
if self.client.get_node(&scope.id.to_string())
|
|
.map_err(|e| IdentityError::GraphError(e.to_string()))?
|
|
.is_none()
|
|
{
|
|
self.client
|
|
.create_node(NODE_SCOPE, scope.to_value())
|
|
.map_err(|e| IdentityError::GraphError(e.to_string()))?;
|
|
}
|
|
self.client
|
|
.create_edge(&role.id.to_string(), &scope.id.to_string(), EDGE_GRANTS)
|
|
.map_err(|e| IdentityError::GraphError(e.to_string()))
|
|
}
|
|
}
|