Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-010: Cedar Policy Engine para Authorization

Status: Accepted | Implemented Date: 2024-11-01 Deciders: Security Architecture Team Technical Story: Implementing declarative RBAC with audit-friendly policies


Decision

Usar Cedar policy engine para autorización declarativa (no custom RBAC, no Casbin).


Rationale

  1. Declarative Policies: Separar políticas de autorización de lógica de código
  2. Auditable: Políticas versionables en Git, fácil de revisar
  3. AWS Proven: Usado internamente en AWS, production-proven
  4. Type Safe: Schemas para resources y principals
  5. No Vendor Lock-in: Open source, portable

Alternatives Considered

❌ Custom RBAC Implementation

  • Pros: Full control
  • Cons: Mantenimiento pesada, fácil de introducir vulnerabilidades

❌ Casbin (Policy Engine)

  • Pros: Flexible
  • Cons: Menos maduro en Rust ecosystem que Cedar

✅ Cedar (CHOSEN)

  • Declarative, auditable, production-proven, AWS-backed

Trade-offs

Pros:

  • ✅ Declarative policies separate from code
  • ✅ Easy to audit and version control
  • ✅ Type-safe schema validation
  • ✅ AWS production-proven
  • ✅ Support for complex hierarchies (teams, orgs)

Cons:

  • ⚠️ Learning curve (new policy language)
  • ⚠️ Policies must be pre-compiled for performance
  • ⚠️ Smaller community than Casbin

Implementation

Policy Definition:

// policies/authorization.cedar

// Allow owners full access to projects
permit(
    principal,
    action,
    resource
)
when {
    principal.role == "owner"
};

// Allow members to create tasks
permit(
    principal in [User],
    action == Action::"create_task",
    resource in [Project]
)
when {
    principal.team_id == resource.team_id &&
    principal.role in ["owner", "member"]
};

// Deny editing completed tasks
forbid(
    principal,
    action == Action::"update_task",
    resource in [Task]
)
when {
    resource.status == "done"
};

// Allow viewing with viewer role
permit(
    principal,
    action == Action::"read",
    resource
)
when {
    principal.role == "viewer"
};

Authorization Check in Backend:

#![allow(unused)]
fn main() {
// crates/vapora-backend/src/api/projects.rs
use cedar_policy::{Authorizer, Request, Entity, Entities};

async fn get_project(
    State(app_state): State<AppState>,
    Path(project_id): Path<String>,
) -> Result<Json<Project>, ApiError> {
    let user = get_current_user()?;

    // Create authorization request
    let request = Request::new(
        user.into_entity(),
        action("read"),
        resource("project", &project_id),
        None,
    )?;

    // Load policies and entities
    let policies = app_state.cedar_policies();
    let entities = app_state.cedar_entities();

    // Authorize
    let authorizer = Authorizer::new();
    let response = authorizer.is_authorized(&request, &policies, &entities)?;

    match response.decision {
        Decision::Allow => {
            let project = app_state
                .project_service
                .get_project(&user.tenant_id, &project_id)
                .await?;
            Ok(Json(project))
        }
        Decision::Deny => Err(ApiError::Forbidden),
    }
}
}

Entity Schema:

#![allow(unused)]
fn main() {
// crates/vapora-backend/src/auth/entities.rs
pub struct User {
    pub id: String,
    pub role: UserRole,
    pub tenant_id: String,
}

pub struct Project {
    pub id: String,
    pub tenant_id: String,
    pub status: ProjectStatus,
}

// Convert to Cedar entities
impl From<User> for cedar_policy::Entity {
    fn from(user: User) -> Self {
        // Serialized to Cedar format
    }
}
}

Key Files:

  • /crates/vapora-backend/src/auth/ (Cedar integration)
  • /crates/vapora-backend/src/api/ (authorization checks)
  • /policies/authorization.cedar (policy definitions)

Verification

# Validate policy syntax
cedar validate --schema schemas/schema.json --policies policies/authorization.cedar

# Test authorization decision
cedar evaluate \
  --schema schemas/schema.json \
  --policies policies/authorization.cedar \
  --entities entities.json \
  --request '{"principal": "User:alice", "action": "Action::read", "resource": "Project:123"}'

# Run authorization tests
cargo test -p vapora-backend test_cedar_authorization

# Test edge cases
cargo test -p vapora-backend test_forbidden_access
cargo test -p vapora-backend test_hierarchical_permissions

Expected Output:

  • Policies validate without syntax errors
  • Owners have full access
  • Members can create tasks in their team
  • Viewers can only read
  • Completed tasks cannot be edited
  • All tests pass

Consequences

Authorization Model

  • Three roles: Owner, Member, Viewer
  • Hierarchical teams (can nest permissions)
  • Resource-scoped access (per project, per task)
  • Audit trail of policy decisions

Policy Management

  • Policies versioned in Git
  • Policy changes require code review
  • Centralized policy repository
  • No runtime policy compilation (pre-compiled)

Performance

  • Policy evaluation cached (policies don't change often)
  • Entity resolution cached per request
  • Negligible latency overhead (<1ms)

Scaling

  • Policies apply across all services
  • Cedar policies portable to other services
  • Centralized policy management

References


Related ADRs: ADR-009 (Istio), ADR-025 (Multi-Tenancy)