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.
Installation
Section titled “Installation”[dependencies]fraiseql-rust = "1.0.0"Requirements: Rust 1.75+ (stable)
What This Crate Provides
Section titled “What This Crate Provides”| Export | Purpose |
|---|---|
AuthorizeBuilder | Fluent builder for custom authorization rules |
AuthorizeConfig | Configuration struct for custom rules |
RoleRequiredBuilder | Fluent builder for role-based access control |
RoleRequiredConfig | Configuration struct for role requirements |
RoleMatchStrategy | Enum: Any, All, Exactly |
AuthzPolicyBuilder | Fluent builder for named authorization policies |
AuthzPolicyConfig | Configuration struct for policies |
AuthzPolicyType | Enum: Rbac, Abac, Custom, Hybrid |
Field | Field definition with optional scope metadata |
SchemaRegistry | Registry for tracking type field scope requirements |
validate_scope | Validates scope format (action:resource) |
ScopeValidationError | Error type for scope validation failures |
Custom Authorization Rules
Section titled “Custom Authorization Rules”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 resourcelet 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 adminslet admin_delete_rule = AuthorizeBuilder::new() .rule("hasRole($context, 'admin')") .description("Admin-only delete") .operations("delete") .recursive(false) .build();AuthorizeBuilder Methods
Section titled “AuthorizeBuilder Methods”| Method | Description |
|---|---|
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 |
Role-Based Access Control
Section titled “Role-Based Access Control”Use RoleRequiredBuilder to require specific roles for access. Supports matching strategies and role hierarchy:
use fraiseql_rust::{RoleRequiredBuilder, RoleMatchStrategy};
// Require any one of multiple roleslet manager_or_director = RoleRequiredBuilder::new() .roles(vec!["manager", "director"]) .strategy(RoleMatchStrategy::Any) .description("Management access") .build();
// Require all roles simultaneouslylet auditor_and_compliance = RoleRequiredBuilder::new() .roles(vec!["auditor", "compliance"]) .strategy(RoleMatchStrategy::All) .description("Requires both auditor and compliance roles") .build();
// Single admin role requirementlet admin_only = RoleRequiredBuilder::new() .roles(vec!["admin"]) .strategy(RoleMatchStrategy::Any) .error_message("Administrator access required") .cacheable(true) .cache_duration_seconds(600) .build();RoleMatchStrategy
Section titled “RoleMatchStrategy”| Variant | Behavior |
|---|---|
RoleMatchStrategy::Any | Caller must hold at least one of the listed roles |
RoleMatchStrategy::All | Caller must hold all listed roles |
RoleMatchStrategy::Exactly | Caller must hold exactly these roles (no more, no less) |
RoleRequiredBuilder Methods
Section titled “RoleRequiredBuilder Methods”| Method | Description |
|---|---|
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 |
Authorization Policies
Section titled “Authorization Policies”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-basedlet 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-basedlet 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 policylet 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 checkslet financial_policy = AuthzPolicyBuilder::new("financialAccess") .policy_type(AuthzPolicyType::Hybrid) .rule("hasRole($context, 'finance')") .attributes(vec!["department == 'finance'"]) .operations("read") .audit_logging(true) .build();AuthzPolicyType
Section titled “AuthzPolicyType”| Variant | Description |
|---|---|
AuthzPolicyType::Rbac | Role-based: uses role expressions |
AuthzPolicyType::Abac | Attribute-based: evaluates attribute conditions |
AuthzPolicyType::Custom | Custom rule expression |
AuthzPolicyType::Hybrid | Combines multiple authorization approaches |
Field-Level Scope Tracking
Section titled “Field-Level Scope Tracking”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 requirementslet 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 requirementslet scoped = registry.extract_scoped_fields();// Returns: { "User": ["email", "salary"] }
// Export to JSON for compilerlet json = registry.export_to_json();Scope Validation
Section titled “Scope Validation”Use validate_scope to validate scope string format before storing or emitting them:
use fraiseql_rust::{validate_scope, ScopeValidationError};
// Valid scopesassert!(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 scopesassert!(validate_scope("readuser").is_err()); // Missing colonassert!(validate_scope("read-all:user").is_err()); // Hyphen in actionassert!(validate_scope("").is_err()); // Empty stringScope format: action:resource
action: alphanumeric + underscores, e.g.,read,write,adminresource: alphanumeric + underscores + dots, or*wildcard- Examples:
read:user.email,write:User.*,admin:*
Serialization
Section titled “Serialization”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",// ...// }Complete example
Section titled “Complete example”For a complete example combining authorization, schema registry, and field-level scoping, see the fraiseql-starter-blog repository.
Transport Annotations
Section titled “Transport Annotations”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.
FraiseQL Client (fraiseql-client)
Section titled “FraiseQL Client (fraiseql-client)”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.
Installation
Section titled “Installation”[dependencies]fraiseql-client = "2.1.0"
# Optional: Candle ML integration for embedding storage/retrievalfraiseql-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(())}Retry configuration
Section titled “Retry configuration”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();Error types
Section titled “Error types”| Variant | When returned |
|---|---|
FraiseQLError::GraphQL | Response contains errors array |
FraiseQLError::Network | Connection refused, DNS failure |
FraiseQLError::Timeout | Request exceeded timeout |
FraiseQLError::Authentication | HTTP 401 or 403 |
FraiseQLError::RateLimit | HTTP 429 (includes retry_after if present) |
FraiseQLError::Serialization | JSON deserialization failure |
Candle ML integration
Section titled “Candle ML integration”With the candle feature enabled, store and retrieve embeddings via FraiseQL mutations:
use candle_core::{Device, Tensor};use serde_json::json;
// Store an embeddinglet 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 embeddingslet tensor = client.fetch_embeddings( "{ embedding(docId: \"doc-123\") { values } }", None, &[3], // shape: 1D tensor with 3 elements).await?;Next Steps
Section titled “Next Steps”- SDK Overview — how compile-time authoring works
- SQL Patterns — view and function conventions
- Security — authentication, RBAC, field-level scopes
- All SDKs — compare languages