This repository has been archived on 2026-08-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-retired/ui/vessels/el-identity/src/guard.rs
T

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()))
}
}