Custom Expressions
Extend the security expression language with your own functions.
The ExpressionRoot Trait
Custom expression functions are added by implementing ExpressionRoot:
use actix_security::http::security::expression::ExpressionRoot;
use actix_security::http::security::User;
pub trait ExpressionRoot: Send + Sync {
/// Evaluate a custom function.
///
/// Returns:
/// - `Some(true)` - Function matched and returned true
/// - `Some(false)` - Function matched and returned false
/// - `None` - Function not recognized, try default implementation
fn evaluate_function(
&self,
name: &str,
args: &[String],
user: Option<&User>,
) -> Option<bool>;
}
Creating a Custom ExpressionRoot
use actix_security::http::security::expression::ExpressionRoot;
use actix_security::http::security::User;
use std::collections::HashSet;
#[derive(Clone)]
pub struct CustomExpressionRoot {
premium_users: HashSet<String>,
beta_features: HashSet<String>,
}
impl ExpressionRoot for CustomExpressionRoot {
fn evaluate_function(
&self,
name: &str,
args: &[String],
user: Option<&User>,
) -> Option<bool> {
match name {
// isPremium() - check if user has premium subscription
"isPremium" => {
let username = user?.username.clone();
Some(self.premium_users.contains(&username))
}
// hasBetaAccess('feature') - check beta feature access
"hasBetaAccess" => {
let feature = args.get(0)?;
let user = user?;
// Admins always have beta access
if user.has_role("ADMIN") {
return Some(true);
}
// Check if feature is in beta and user has beta role
Some(
self.beta_features.contains(feature)
&& user.has_role("BETA_TESTER"),
)
}
// isOwner('resource_id') - check resource ownership
"isOwner" => {
let resource_id = args.get(0)?;
let user = user?;
// Your ownership logic
Some(self.check_ownership(&user.username, resource_id))
}
// Unknown function - return None to use default
_ => None,
}
}
}
impl CustomExpressionRoot {
fn check_ownership(&self, username: &str, resource_id: &str) -> bool {
// Your database lookup logic
true
}
}
Registering Custom Expressions
Register your custom ExpressionRoot with the security configuration:
use actix_security::http::security::expression::ExpressionEvaluator;
let custom_root = CustomExpressionRoot {
premium_users: vec!["vip_user".to_string()].into_iter().collect(),
beta_features: vec!["new_dashboard".to_string()].into_iter().collect(),
};
// Create evaluator with custom root
let evaluator = ExpressionEvaluator::with_root(Box::new(custom_root));
Using Custom Functions
Once registered, use your custom functions in expressions:
// Check premium status
#[pre_authorize("isPremium()")]
#[get("/premium/content")]
async fn premium_content(user: AuthenticatedUser) -> impl Responder {
HttpResponse::Ok().body("Premium content")
}
// Check beta access
#[pre_authorize("hasBetaAccess('new_dashboard')")]
#[get("/beta/dashboard")]
async fn beta_dashboard(user: AuthenticatedUser) -> impl Responder {
HttpResponse::Ok().body("Beta dashboard")
}
// Combine with built-in functions
#[pre_authorize("hasRole('USER') AND isPremium()")]
#[get("/premium/profile")]
async fn premium_profile(user: AuthenticatedUser) -> impl Responder {
HttpResponse::Ok().body("Premium profile")
}
// Complex custom expression
#[pre_authorize("hasRole('ADMIN') OR (hasRole('USER') AND hasBetaAccess('feature'))")]
#[get("/feature")]
async fn feature(user: AuthenticatedUser) -> impl Responder {
HttpResponse::Ok().body("Feature")
}
Spring Security Comparison
Spring Security:
// Custom SecurityExpressionRoot
public class CustomSecurityExpressionRoot
extends SecurityExpressionRoot
implements MethodSecurityExpressionOperations {
public boolean isPremium() {
return premiumService.isPremium(getAuthentication().getName());
}
public boolean hasBetaAccess(String feature) {
return betaService.hasAccess(getAuthentication(), feature);
}
}
// Usage
@PreAuthorize("isPremium()")
public void premiumContent() {}
@PreAuthorize("hasBetaAccess('new_feature')")
public void betaFeature() {}
Actix Security:
// Custom ExpressionRoot
impl ExpressionRoot for CustomExpressionRoot {
fn evaluate_function(&self, name: &str, args: &[String], user: Option<&User>) -> Option<bool> {
match name {
"isPremium" => Some(self.premium_service.is_premium(user?)),
"hasBetaAccess" => Some(self.beta_service.has_access(user?, args.get(0)?)),
_ => None,
}
}
}
// Usage
#[pre_authorize("isPremium()")]
async fn premium_content() {}
#[pre_authorize("hasBetaAccess('new_feature')")]
async fn beta_feature() {}
Example: Organization-Based Access
#[derive(Clone)]
pub struct OrgExpressionRoot {
org_service: OrgService,
}
impl ExpressionRoot for OrgExpressionRoot {
fn evaluate_function(
&self,
name: &str,
args: &[String],
user: Option<&User>,
) -> Option<bool> {
match name {
// belongsToOrg('org_id') - user belongs to organization
"belongsToOrg" => {
let org_id = args.get(0)?;
let user = user?;
Some(self.org_service.user_belongs_to(&user.username, org_id))
}
// isOrgAdmin('org_id') - user is admin of organization
"isOrgAdmin" => {
let org_id = args.get(0)?;
let user = user?;
Some(self.org_service.is_org_admin(&user.username, org_id))
}
// hasOrgPermission('org_id', 'permission')
"hasOrgPermission" => {
let org_id = args.get(0)?;
let permission = args.get(1)?;
let user = user?;
Some(self.org_service.has_permission(&user.username, org_id, permission))
}
_ => None,
}
}
}
// Usage
#[pre_authorize("belongsToOrg('acme-corp')")]
async fn org_dashboard() {}
#[pre_authorize("isOrgAdmin('acme-corp') OR hasRole('SUPER_ADMIN')")]
async fn org_settings() {}
#[pre_authorize("hasOrgPermission('acme-corp', 'billing:manage')")]
async fn billing() {}
Best Practices
1. Return None for Unknown Functions
Allow fallback to default implementation:
fn evaluate_function(&self, name: &str, args: &[String], user: Option<&User>) -> Option<bool> {
match name {
"myFunction" => Some(/* ... */),
_ => None, // Important: let default handle unknown functions
}
}
2. Handle Missing User
Return false or None when user is required but missing:
"isPremium" => {
let user = user?; // Returns None if no user
Some(self.check_premium(&user.username))
}
3. Validate Arguments
Check for required arguments:
"hasFeature" => {
let feature = args.get(0)?; // Returns None if missing
Some(self.check_feature(feature))
}
4. Keep Functions Simple
Complex logic should live in services:
// Good
"isPremium" => Some(self.premium_service.is_premium(user?))
// Bad - too much logic in expression root
"isPremium" => {
let user = user?;
let subscription = db.query_subscription(&user.id)?;
Some(subscription.tier == "premium" && subscription.expires > now())
}
5. Document Your Functions
/// Custom expression functions for MyApp.
///
/// Available functions:
/// - `isPremium()` - Returns true if user has premium subscription
/// - `hasBetaAccess('feature')` - Returns true if user can access beta feature
/// - `isOrgMember('org_id')` - Returns true if user belongs to organization
impl ExpressionRoot for MyExpressionRoot { /* ... */ }