Complete implementation of link-level authorization in the this-rs framework, allowing you to define specific permissions for each link type independently of entity permissions.
LinkAuthConfig Structure (src/core/link.rs)#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinkAuthConfig {
pub create: AuthPolicy, // Policy for creating links
pub delete: AuthPolicy, // Policy for deleting links
pub update: AuthPolicy, // Policy for updating link metadata
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthPolicy {
pub policy: String, // Policy type (e.g., "AllowOwner", "RequireRole")
pub roles: Vec<String>, // Required roles
}
Features:
authenticatedDefault trait implementedLinkDefinition (src/core/link.rs)pub struct LinkDefinition {
// ... existing fields
/// Authorization configuration specific to this link type
#[serde(default)]
pub auth: Option<LinkAuthConfig>,
}
Advantages:
Option<LinkAuthConfig> allows fallback to entity auth#[serde(default)] ensures backward compatibilityauth continue to worklinks:
- link_type: owner
source_type: user
target_type: car
forward_route_name: cars-owned
reverse_route_name: owner
auth:
create:
policy: AllowOwner
roles: ["admin", "user"]
delete:
policy: RequireRole
roles: ["admin"]
update:
policy: AllowOwner
roles: ["admin", "user"]
βββββββββββββββββββββββββββββββββββββββββββ
β Request Arrives β
ββββββββββββββββ¬βββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β 1. Check Link-Level Auth Config β
β (from LinkDefinition.auth) β
ββββββββββββββββ¬βββββββββββββββββββββββββββ
β
ββ Has link auth? ββYESβββΊ Use link-specific policies
β (create/delete/update)
β
ββ No link auth? ββNOββββΊ Fallback to entity auth
(if implemented)
links:
# Anyone can add a car they own
- link_type: owner
source_type: user
target_type: car
forward_route_name: cars-owned
auth:
create:
policy: Authenticated
roles: []
delete:
policy: AllowOwner
roles: []
# Only admins can assign drivers
- link_type: driver
source_type: user
target_type: car
forward_route_name: cars-driven
auth:
create:
policy: RequireRole
roles: ["admin"]
delete:
policy: RequireRole
roles: ["admin"]
links:
# Orders can be linked to invoices by users
- link_type: has_invoice
source_type: order
target_type: invoice
forward_route_name: invoices
auth:
create:
policy: Authenticated
roles: ["user", "admin"]
delete:
policy: RequireRole
roles: ["admin"] # Only admins can unlink
# Invoices can be linked to payments (stricter)
- link_type: has_payment
source_type: invoice
target_type: payment
forward_route_name: payments
auth:
create:
policy: RequireRole
roles: ["accounting", "admin"]
delete:
policy: RequireRole
roles: ["admin"] # Only admins can unlink payments
policy: Authenticated
roles: []
Any authenticated user can perform the action.
policy: AllowOwner
roles: ["user"]
User must own one of the linked entities AND have one of the specified roles.
policy: RequireRole
roles: ["admin", "manager"]
User must have at least one of the specified roles.
policy: CustomPolicy
roles: ["special"]
Implement your own AuthProvider to handle custom policies.
impl AppState {
pub fn get_link_auth_policy(
link_definition: &LinkDefinition,
operation: &str,
) -> Option<String> {
link_definition.auth.as_ref().and_then(|auth| {
match operation {
"create" => Some(auth.create.policy.clone()),
"delete" => Some(auth.delete.policy.clone()),
"update" => Some(auth.update.policy.clone()),
_ => None,
}
})
}
}
pub async fn create_link(
State(state): State<AppState>,
Path((source_type, source_id, route_name, target_id)): Path<(String, Uuid, String, Uuid)>,
// auth_context: AuthContext, // If using auth
Json(payload): Json<CreateLinkRequest>,
) -> Result<Response, ExtractorError> {
let extractor = DirectLinkExtractor::from_path(...)?;
// Check authorization
if let Some(policy) = AppState::get_link_auth_policy(
&extractor.link_definition,
"create"
) {
// Validate policy (if auth provider is configured)
// auth_provider.check_policy(&auth_context, &policy, &extractor)?;
}
// Create the link
let link = LinkEntity::new(...);
state.link_service.create(link).await?;
Ok(...)
}
#[test]
fn test_link_definition_with_auth() {
let yaml = r#"
link_type: owner
source_type: user
target_type: car
forward_route_name: cars-owned
reverse_route_name: owner
auth:
create:
policy: AllowOwner
roles: [admin, user]
delete:
policy: RequireRole
roles: [admin]
"#;
let def: LinkDefinition = serde_yaml::from_str(yaml).unwrap();
assert!(def.auth.is_some());
let auth = def.auth.unwrap();
assert_eq!(auth.create.policy, "AllowOwner");
assert_eq!(auth.create.roles, vec!["admin", "user"]);
}
# Should succeed (user is owner)
curl -X POST http://localhost:3000/users/123/cars-owned/456 \
-H "Authorization: Bearer user-token"
# Should fail (user is not admin)
curl -X DELETE http://localhost:3000/users/123/drivers/456 \
-H "Authorization: Bearer user-token"
# Returns: 403 Forbidden
# Should succeed (user is admin)
curl -X DELETE http://localhost:3000/users/123/drivers/456 \
-H "Authorization: Bearer admin-token"
Different link types between the same entities can have different permissions:
# User β Car (owner): Anyone authenticated
- link_type: owner
auth:
create:
policy: Authenticated
# User β Car (driver): Only admins
- link_type: driver
auth:
create:
policy: RequireRole
roles: [admin]
Control who can create/delete links at different workflow stages:
# Create invoice link: Any user
# Delete invoice link: Only admins
- link_type: has_invoice
auth:
create:
policy: Authenticated
roles: []
delete:
policy: RequireRole
roles: [admin]
Entity permissions and link permissions are separate:
All authorization rules in one place (YAML), easy to audit and modify.
// Authorization checked at entity level
if !user.can_create_order() {
return Err(StatusCode::FORBIDDEN);
}
order_service.create(order).await?;
# Configuration-driven authorization
links:
- link_type: has_invoice
auth:
create:
policy: RequireRole
roles: [sales, admin]
No code changes needed! Authorization is declarative.
Link-level authorization provides:
β
Fine-grained control - Different permissions per link type
β
Declarative - All rules in YAML configuration
β
Independent - Separate from entity permissions
β
Flexible - Multiple policy types supported
β
Backward compatible - Links without auth still work
Perfect for complex workflows and multi-tenant scenarios! ππβ¨