Skip to content

Rust Authorization Library

The fraiseql-rust crate provides authorization and RBAC configuration utilities for FraiseQL schemas. It gives you type-safe, fluent builders for defining custom authorization rules, role-based access control, and named authorization policies.

Cargo.toml
[dependencies]
fraiseql-rust = "1.0.0"

Requirements: Rust 1.75+ (stable)

ExportPurpose
AuthorizeBuilderFluent builder for custom authorization rules
AuthorizeConfigConfiguration struct for custom rules
RoleRequiredBuilderFluent builder for role-based access control
RoleRequiredConfigConfiguration struct for role requirements
RoleMatchStrategyEnum: Any, All, Exactly
AuthzPolicyBuilderFluent builder for named authorization policies
AuthzPolicyConfigConfiguration struct for policies
AuthzPolicyTypeEnum: Rbac, Abac, Custom, Hybrid
FieldField definition with optional scope metadata
SchemaRegistryRegistry for tracking type field scope requirements
validate_scopeValidates scope format (action:resource)
ScopeValidationErrorError type for scope validation failures

Use AuthorizeBuilder to define expression-based authorization rules. Rules are attached to fields or types in the compiled schema:

use fraiseql_rust::{AuthorizeBuilder, AuthorizeConfig};
// Ownership check: user must own the resource
let ownership_rule = AuthorizeBuilder::new()
.rule("isOwner($context.userId, $resource.ownerId)")
.description("User must own the resource")
.error_message("Access denied: you do not own this resource")
.cacheable(true)
.cache_duration_seconds(300)
.build();
// Operation-specific rule: restrict delete to admins
let admin_delete_rule = AuthorizeBuilder::new()
.rule("hasRole($context, 'admin')")
.description("Admin-only delete")
.operations("delete")
.recursive(false)
.build();
MethodDescription
rule(expr)Authorization rule expression
policy(name)Reference a named policy by name
description(text)Human-readable description
error_message(msg)Custom error message on denial
recursive(bool)Apply rule recursively to nested types
operations(ops)Comma-separated operation list: "read", "create,update", etc.
cacheable(bool)Enable authorization result caching
cache_duration_seconds(u32)Cache TTL in seconds (default: 300)
build()Returns AuthorizeConfig

Use RoleRequiredBuilder to require specific roles for access. Supports matching strategies and role hierarchy:

use fraiseql_rust::{RoleRequiredBuilder, RoleMatchStrategy};
// Require any one of multiple roles
let manager_or_director = RoleRequiredBuilder::new()
.roles(vec!["manager", "director"])
.strategy(RoleMatchStrategy::Any)
.description("Management access")
.build();
// Require all roles simultaneously
let auditor_and_compliance = RoleRequiredBuilder::new()
.roles(vec!["auditor", "compliance"])
.strategy(RoleMatchStrategy::All)
.description("Requires both auditor and compliance roles")
.build();
// Single admin role requirement
let admin_only = RoleRequiredBuilder::new()
.roles(vec!["admin"])
.strategy(RoleMatchStrategy::Any)
.error_message("Administrator access required")
.cacheable(true)
.cache_duration_seconds(600)
.build();
VariantBehavior
RoleMatchStrategy::AnyCaller must hold at least one of the listed roles
RoleMatchStrategy::AllCaller must hold all listed roles
RoleMatchStrategy::ExactlyCaller must hold exactly these roles (no more, no less)
MethodDescription
roles(iter)Required roles (any iterator of string-like items)
roles_vec(vec)Required roles from a Vec<String>
strategy(RoleMatchStrategy)Role matching strategy (default: Any)
hierarchy(bool)Enable role hierarchy resolution
inherit(bool)Inherit role requirements from parent
description(text)Human-readable description
error_message(msg)Custom error on denial
operations(ops)Operation-specific requirements
cacheable(bool)Enable caching
cache_duration_seconds(u32)Cache TTL (default: 300)
build()Returns RoleRequiredConfig

Named authorization policies can be defined once and referenced by name across multiple fields. Use AuthzPolicyBuilder to define reusable policies:

use fraiseql_rust::{AuthzPolicyBuilder, AuthzPolicyType};
// RBAC policy: role-based
let pii_access = AuthzPolicyBuilder::new("piiAccess")
.policy_type(AuthzPolicyType::Rbac)
.rule("hasRole($context, 'data_manager')")
.description("PII data access requires data_manager role")
.audit_logging(true)
.build();
// ABAC policy: attribute-based
let clearance_policy = AuthzPolicyBuilder::new("secretClearance")
.policy_type(AuthzPolicyType::Abac)
.attributes(vec!["clearance_level >= 3", "background_check == true"])
.description("Top secret clearance required")
.cacheable(true)
.build();
// Custom rule policy
let ownership_policy = AuthzPolicyBuilder::new("ownerOnly")
.policy_type(AuthzPolicyType::Custom)
.rule("isOwner($context.userId, $resource.ownerId)")
.description("Resource must be owned by the requester")
.build();
// Hybrid policy combining multiple checks
let financial_policy = AuthzPolicyBuilder::new("financialAccess")
.policy_type(AuthzPolicyType::Hybrid)
.rule("hasRole($context, 'finance')")
.attributes(vec!["department == 'finance'"])
.operations("read")
.audit_logging(true)
.build();
VariantDescription
AuthzPolicyType::RbacRole-based: uses role expressions
AuthzPolicyType::AbacAttribute-based: evaluates attribute conditions
AuthzPolicyType::CustomCustom rule expression
AuthzPolicyType::HybridCombines multiple authorization approaches

Field and SchemaRegistry let you define and track field-level scope requirements for your schema. This metadata is exported to the compiled schema JSON:

use fraiseql_rust::{Field, SchemaRegistry};
let mut registry = SchemaRegistry::new();
// Define User type with field-level scope requirements
let user_fields = vec![
Field::new("id", "ID")
.with_nullable(false),
Field::new("username", "String")
.with_nullable(false),
Field::new("email", "String")
.with_nullable(false)
.with_requires_scope(Some("read:user.email".to_string())),
Field::new("salary", "Float")
.with_nullable(true)
.with_requires_scopes(Some(vec![
"read:user.salary".to_string(),
"admin".to_string(),
])),
];
registry.register_type("User", user_fields);
// Extract which fields have scope requirements
let scoped = registry.extract_scoped_fields();
// Returns: { "User": ["email", "salary"] }
// Export to JSON for compiler
let json = registry.export_to_json();

Use validate_scope to validate scope string format before storing or emitting them:

use fraiseql_rust::{validate_scope, ScopeValidationError};
// Valid scopes
assert!(validate_scope("read:user.email").is_ok());
assert!(validate_scope("admin:*").is_ok());
assert!(validate_scope("read:User.*").is_ok());
assert!(validate_scope("*").is_ok()); // Global wildcard
// Invalid scopes
assert!(validate_scope("readuser").is_err()); // Missing colon
assert!(validate_scope("read-all:user").is_err()); // Hyphen in action
assert!(validate_scope("").is_err()); // Empty string

Scope format: action:resource

  • action: alphanumeric + underscores, e.g., read, write, admin
  • resource: alphanumeric + underscores + dots, or * wildcard
  • Examples: read:user.email, write:User.*, admin:*

All config structs provide to_map() for serialization to HashMap<String, String>, suitable for embedding in schema JSON:

use fraiseql_rust::RoleRequiredBuilder;
let config = RoleRequiredBuilder::new()
.roles(vec!["admin"])
.build();
let map = config.to_map();
// {
// "roles": "admin",
// "strategy": "any",
// "cacheable": "true",
// "cacheDurationSeconds": "300",
// ...
// }

For a complete example combining authorization, schema registry, and field-level scoping, see the fraiseql-starter-blog repository.


Transport annotations are optional on schema operations authored with other SDKs. When using fraiseql-rust for Rust-native schema authoring (separate from the authorization library), add rest_path and rest_method to the #[fraiseql::query] and #[fraiseql::mutation] proc-macro attributes. gRPC endpoints are auto-generated when [grpc] is enabled — no per-operation annotation needed. See gRPC Transport.

use fraiseql_sdk::prelude::*;
// REST + GraphQL
#[fraiseql::query(
sql_source = "v_post",
rest_path = "/posts",
rest_method = "GET",
)]
async fn posts(limit: Option<i32>) -> Vec<Post> { vec![] }
// With path parameter
#[fraiseql::query(
sql_source = "v_post",
rest_path = "/posts/{id}",
rest_method = "GET",
)]
async fn post(id: Uuid) -> Option<Post> { None }
// REST mutation
#[fraiseql::mutation(
sql_source = "create_post",
operation = "CREATE",
rest_path = "/posts",
rest_method = "POST",
)]
async fn create_post(title: String, author_id: Uuid) -> Post { unimplemented!() }

Path parameters in rest_path (e.g., {id}) must match function argument names exactly. A mismatch produces a compile-time error. Duplicate (method, path) pairs are also rejected at compile time.



The fraiseql-client crate is a separate async HTTP client for consuming FraiseQL GraphQL APIs from Rust applications. It is independent of the authorization library above.

Cargo.toml
[dependencies]
fraiseql-client = "2.1.0"
# Optional: Candle ML integration for embedding storage/retrieval
fraiseql-client = { version = "2.1.0", features = ["candle"] }
use fraiseql_client::FraiseQLClientBuilder;
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize)]
struct User {
id: String,
name: String,
}
#[tokio::main]
async fn main() -> Result<(), fraiseql_client::FraiseQLError> {
let client = FraiseQLClientBuilder::new("http://localhost:8080/graphql")
.authorization("Bearer eyJhbGciOiJIUzI1NiIs...")
.timeout(std::time::Duration::from_secs(30))
.build();
// Query
let users: Vec<User> = client.query(
"{ users { id name } }",
None,
).await?;
// Mutation with variables
let new_user: User = client.mutate(
r#"mutation($name: String!, $email: String!) {
createUser(name: $name, email: $email) { id name }
}"#,
Some(&json!({ "name": "Alice", "email": "alice@example.com" })),
).await?;
Ok(())
}
use fraiseql_client::{FraiseQLClientBuilder, RetryConfig};
use std::time::Duration;
let client = FraiseQLClientBuilder::new("http://localhost:8080/graphql")
.retry(RetryConfig {
max_attempts: 3,
base_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(30),
jitter: true,
})
.build();
VariantWhen returned
FraiseQLError::GraphQLResponse contains errors array
FraiseQLError::NetworkConnection refused, DNS failure
FraiseQLError::TimeoutRequest exceeded timeout
FraiseQLError::AuthenticationHTTP 401 or 403
FraiseQLError::RateLimitHTTP 429 (includes retry_after if present)
FraiseQLError::SerializationJSON deserialization failure

With the candle feature enabled, store and retrieve embeddings via FraiseQL mutations:

use candle_core::{Device, Tensor};
use serde_json::json;
// Store an embedding
let embedding = Tensor::new(&[0.1f32, 0.2, 0.3], &Device::Cpu)?;
let result = client.store_embedding(
"mutation($embedding: [Float!]!, $docId: ID!) { storeEmbedding(embedding: $embedding, docId: $docId) { id } }",
&embedding,
json!({ "docId": "doc-123" }),
).await?;
// Fetch embeddings
let tensor = client.fetch_embeddings(
"{ embedding(docId: \"doc-123\") { values } }",
None,
&[3], // shape: 1D tensor with 3 elements
).await?;