Skip to main content

typesec_integrations/jwt/
engine.rs

1//! Policy engine backed by verified JWT permission claims.
2
3use std::collections::HashSet;
4
5use serde_json::Value;
6use tracing::debug;
7use typesec_core::{
8    ResourceId, SubjectId,
9    policy::{PolicyEngine, PolicyResult},
10};
11
12use super::claims::VerifiedSubject;
13
14/// Policy engine backed by verified JWT permission claims.
15///
16/// This is intended as the fast first layer in a composed engine: allow obvious
17/// org-wide permissions from the token and delegate resource-specific decisions
18/// to RBAC, ODRL, WorkOS FGA, or another precise engine.
19pub struct JwtClaimsEngine {
20    subject: String,
21    permissions: HashSet<String>,
22    org_id: Option<String>,
23}
24
25impl JwtClaimsEngine {
26    /// Build an engine from a verified subject.
27    pub fn new(subject: VerifiedSubject) -> Self {
28        Self {
29            subject: subject.subject,
30            permissions: subject.permissions.into_iter().collect(),
31            org_id: subject.org_id,
32        }
33    }
34
35    /// Build an engine from raw permission strings.
36    pub fn from_permissions(
37        subject: impl Into<String>,
38        permissions: impl IntoIterator<Item = String>,
39    ) -> Self {
40        Self {
41            subject: subject.into(),
42            permissions: permissions.into_iter().collect(),
43            org_id: None,
44        }
45    }
46
47    fn permission_matches(&self, action: &str, resource: &str) -> bool {
48        if self.permissions.contains(action) {
49            return true;
50        }
51
52        let resource_type = resource.split(['/', ':']).next().unwrap_or(resource);
53        self.permissions
54            .contains(&format!("{resource_type}:{action}"))
55    }
56}
57
58impl PolicyEngine for JwtClaimsEngine {
59    fn check(&self, subject: &SubjectId, action: &str, resource: &ResourceId) -> PolicyResult {
60        let subject = subject.as_str();
61        let resource = resource.as_str();
62        debug!(subject, action, resource, org_id = ?self.org_id, "jwt claims check");
63
64        if subject != self.subject {
65            return PolicyResult::delegate(
66                "jwt",
67                format!("jwt claims are for '{}', not '{subject}'", self.subject),
68            );
69        }
70
71        if self.permission_matches(action, resource) {
72            PolicyResult::Allow
73        } else {
74            PolicyResult::delegate(
75                "jwt",
76                format!("permission '{action}' not present in jwt claims"),
77            )
78        }
79    }
80}
81
82#[allow(dead_code)]
83fn _assert_value_send_sync(_: Value) {}