this-rs provides link-level authorization, allowing you to control who can create, update, or delete links independently of entity permissions.
Scenario: Healthcare System
- Doctors can READ patient records β
- Doctors can CREATE diagnoses β
- But should doctors link ANY diagnosis to ANY patient? β
Solution: Link-level authorization lets you control the relationships themselves.
links:
- link_type: has_diagnosis
source_type: patient
target_type: diagnosis
forward_route_name: diagnoses
reverse_route_name: patient
auth:
create:
policy: RequireRole
roles: [doctor, admin]
delete:
policy: RequireRole
roles: [admin] # Only admins can remove diagnoses
update:
policy: RequireRole
roles: [doctor, admin]
Any authenticated user:
auth:
create:
policy: Authenticated
roles: []
User must have one of the specified roles:
auth:
create:
policy: RequireRole
roles: [doctor, nurse, admin]
User must own one of the linked entities:
auth:
create:
policy: AllowOwner
roles: [user] # Must be a user AND own one entity
Implement custom logic:
auth:
create:
policy: CustomApprovalWorkflow
roles: [manager]
links:
- link_type: owner
source_type: user
target_type: car
forward_route_name: cars-owned
auth:
create:
policy: Authenticated # Anyone can claim ownership
roles: []
delete:
policy: AllowOwner # Only owner can remove
roles: []
update:
policy: AllowOwner # Only owner can update metadata
roles: []
links:
- link_type: approved_by
source_type: document
target_type: user
forward_route_name: approvers
auth:
create:
policy: RequireRole
roles: [manager, director] # Only managers can approve
delete:
policy: RequireRole
roles: [admin] # Only admins can revoke approval
links:
# Regular users can add members
- link_type: has_member
source_type: team
target_type: user
forward_route_name: members
auth:
create:
policy: RequireRole
roles: [team_lead, admin]
delete:
policy: RequireRole
roles: [team_lead, admin]
# Only admins can assign team leads
- link_type: has_lead
source_type: team
target_type: user
forward_route_name: leads
auth:
create:
policy: RequireRole
roles: [admin]
delete:
policy: RequireRole
roles: [admin]
use this::prelude::*;
pub struct YourAuthProvider {
// Your auth logic (JWT validation, DB checks, etc.)
}
#[async_trait]
impl AuthProvider for YourAuthProvider {
async fn check_policy(
&self,
context: &AuthContext,
policy: &str,
// Additional context as needed
) -> Result<bool> {
match policy {
"Authenticated" => Ok(context.is_authenticated()),
"RequireRole" => Ok(context.has_any_role(&required_roles)),
"AllowOwner" => Ok(self.is_owner(context, entity_id).await?),
_ => Ok(false),
}
}
}
pub async fn create_link(
State(state): State<AppState>,
auth_context: AuthContext, // Extract from request
Path((source_type, source_id, route_name, target_id)): Path<(String, Uuid, String, Uuid)>,
Json(payload): Json<CreateLinkRequest>,
) -> Result<Response, ExtractorError> {
let extractor = DirectLinkExtractor::from_path(...)?;
// Check authorization
if let Some(auth_config) = &extractor.link_definition.auth {
let policy = &auth_config.create;
if !state.auth_provider.check_policy(
&auth_context,
&policy.policy,
).await? {
return Err(ExtractorError::Unauthorized);
}
}
// Create the link
let link = LinkEntity::new(...);
state.link_service.create(link).await?;
Ok(...)
}
#[test]
fn test_auth_config_parsing() {
let yaml = r#"
link_type: has_invoice
source_type: order
target_type: invoice
forward_route_name: invoices
auth:
create:
policy: Authenticated
roles: []
delete:
policy: RequireRole
roles: [admin]
"#;
let def: LinkDefinition = serde_yaml::from_str(yaml).unwrap();
assert!(def.auth.is_some());
}
# Should succeed (authenticated user)
curl -X POST http://localhost:3000/orders/123/invoices/456 \
-H "Authorization: Bearer user-token"
# Should fail (not an admin)
curl -X DELETE http://localhost:3000/orders/123/invoices/456 \
-H "Authorization: Bearer user-token"
# Returns: 403 Forbidden
# Should succeed (admin user)
curl -X DELETE http://localhost:3000/orders/123/invoices/456 \
-H "Authorization: Bearer admin-token"
links:
# Anyone can view document relationships
- link_type: references
source_type: document
target_type: document
forward_route_name: references
auth:
create:
policy: Authenticated
roles: []
delete:
policy: AllowOwner
roles: []
# Only specific roles can publish
- link_type: published_in
source_type: document
target_type: collection
forward_route_name: collections
auth:
create:
policy: RequireRole
roles: [editor, publisher, admin]
delete:
policy: RequireRole
roles: [admin]
links:
# Users control their own friendships
- link_type: friend
source_type: user
target_type: user
forward_route_name: friends
auth:
create:
policy: AllowOwner
roles: [user]
delete:
policy: AllowOwner
roles: [user]
# Moderators can block users
- link_type: blocked
source_type: user
target_type: user
forward_route_name: blocked_users
auth:
create:
policy: RequireRole
roles: [moderator, admin]
delete:
policy: RequireRole
roles: [admin]
links:
# Team members can assign tasks
- link_type: assigned_to
source_type: task
target_type: user
forward_route_name: assignees
auth:
create:
policy: RequireRole
roles: [team_member, project_manager]
delete:
policy: RequireRole
roles: [project_manager, admin]
# Only project managers can set dependencies
- link_type: depends_on
source_type: task
target_type: task
forward_route_name: dependencies
auth:
create:
policy: RequireRole
roles: [project_manager, admin]
delete:
policy: RequireRole
roles: [project_manager, admin]
# β
Good: Restrict by default, open as needed
auth:
create:
policy: RequireRole
roles: [specific_role]
# β Avoid: Too permissive
auth:
create:
policy: Authenticated
roles: []
# β
Good: Different rules for creation and deletion
auth:
create:
policy: Authenticated
roles: []
delete:
policy: RequireRole
roles: [admin]
# β
Good: Clear intent
policy: RequireManagerApproval
policy: AllowTeamMembers
# β Avoid: Generic names
policy: Policy1
policy: CheckAccess
# β
Good: Add descriptions
links:
- link_type: approved_by
description: "Links documents to approvers. Only managers can approve."
auth:
create:
policy: RequireRole
roles: [manager]
Link-level authorization gives you fine-grained control over your data relationships! ππβ¨