Skip to main content

hessra_token_authz/
verify.rs

1extern crate biscuit_auth as biscuit;
2
3use biscuit::datalog::RunLimits;
4use biscuit::macros::{authorizer, check, fact};
5use biscuit::Algorithm;
6use chrono::Utc;
7use hessra_token_core::{
8    parse_authorization_failure, parse_check_failure, Biscuit, PublicKey, ServiceChainFailure,
9    TokenError,
10};
11use serde::Deserialize;
12use std::time::Duration;
13
14#[derive(Debug, Deserialize, Clone)]
15pub struct ServiceNode {
16    pub component: String,
17    pub public_key: String,
18}
19
20/// Optional context for token verification restrictions.
21///
22/// This struct bundles optional restrictions that can be applied during
23/// token verification. Using a struct instead of individual parameters
24/// makes it easier to add new restrictions without changing function signatures.
25#[derive(Debug, Clone, Default)]
26pub struct VerificationContext {
27    /// Domain restriction for the verification
28    pub domain: Option<String>,
29    /// Prefix restriction for the verification
30    pub prefix: Option<String>,
31}
32
33/// Verification mode for authorization tokens.
34#[derive(Debug, Clone, PartialEq)]
35enum VerificationMode {
36    /// Identity-based verification: requires an explicit subject parameter.
37    /// The authorizer will check if the token grants rights to this specific subject.
38    Identity { subject: String },
39    /// Capability-based verification: derives the subject from the token's rights.
40    /// The authorizer will accept any token that grants the specified capability
41    /// (resource + operation), regardless of the subject.
42    Capability,
43}
44
45impl VerificationMode {
46    /// Returns the subject if in Identity mode, None for Capability mode.
47    fn subject(&self) -> Option<&str> {
48        match self {
49            VerificationMode::Identity { subject } => Some(subject),
50            VerificationMode::Capability => None,
51        }
52    }
53
54    /// Returns the subject if in Identity mode, "unknown" for Capability mode.
55    /// Used for error reporting.
56    fn subject_or_unknown(&self) -> &str {
57        match self {
58            VerificationMode::Identity { subject } => subject,
59            VerificationMode::Capability => "unknown",
60        }
61    }
62}
63
64/// Builder for verifying Hessra authorization tokens with flexible configuration.
65///
66/// This builder allows you to configure various verification parameters including
67/// optional domain restrictions and service chain attestation.
68///
69/// # Example
70/// ```no_run
71/// use hessra_token_authz::{AuthorizationVerifier, ServiceNode, create_token};
72/// use hessra_token_core::KeyPair;
73///
74/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
75/// // Create a token
76/// let keypair = KeyPair::new();
77/// let public_key = keypair.public();
78/// let token = create_token(
79///     "user123".to_string(),
80///     "resource456".to_string(),
81///     "read".to_string(),
82///     keypair,
83/// )?;
84///
85/// // Basic authorization verification
86/// AuthorizationVerifier::new(
87///     token.clone(),
88///     public_key,
89///     "user123".to_string(),
90///     "resource456".to_string(),
91///     "read".to_string(),
92/// )
93/// .verify()?;
94///
95/// // With domain restriction
96/// AuthorizationVerifier::new(
97///     token.clone(),
98///     public_key,
99///     "user123".to_string(),
100///     "resource456".to_string(),
101///     "read".to_string(),
102/// )
103/// .with_domain("example.com".to_string())
104/// .verify()?;
105///
106/// // With service chain attestation
107/// let service_nodes = vec![
108///     ServiceNode {
109///         component: "api-gateway".to_string(),
110///         public_key: "ed25519/abcd1234...".to_string(),
111///     }
112/// ];
113/// AuthorizationVerifier::new(
114///     token,
115///     public_key,
116///     "user123".to_string(),
117///     "resource456".to_string(),
118///     "read".to_string(),
119/// )
120/// .with_service_chain(service_nodes, Some("api-gateway".to_string()))
121/// .verify()?;
122/// # Ok(())
123/// # }
124/// ```
125pub struct AuthorizationVerifier {
126    token: String,
127    public_key: PublicKey,
128    mode: VerificationMode,
129    resource: String,
130    operation: String,
131    context: VerificationContext,
132    service_chain: Option<(Vec<ServiceNode>, Option<String>)>,
133}
134
135impl AuthorizationVerifier {
136    /// Creates a new authorization verifier for a base64-encoded token.
137    ///
138    /// # Arguments
139    /// * `token` - The base64-encoded authorization token to verify
140    /// * `public_key` - The public key used to verify the token signature
141    /// * `subject` - The subject (user) identifier to verify authorization for
142    /// * `resource` - The resource identifier to verify authorization against
143    /// * `operation` - The operation to verify authorization for
144    pub fn new(
145        token: String,
146        public_key: PublicKey,
147        subject: String,
148        resource: String,
149        operation: String,
150    ) -> Self {
151        Self {
152            token,
153            public_key,
154            mode: VerificationMode::Identity { subject },
155            resource,
156            operation,
157            context: VerificationContext::default(),
158            service_chain: None,
159        }
160    }
161
162    /// Creates a new authorization verifier from raw token bytes.
163    ///
164    /// # Arguments
165    /// * `token` - The raw binary Biscuit token bytes
166    /// * `public_key` - The public key used to verify the token signature
167    /// * `subject` - The subject (user) identifier to verify authorization for
168    /// * `resource` - The resource identifier to verify authorization against
169    /// * `operation` - The operation to verify authorization for
170    pub fn from_bytes(
171        token: Vec<u8>,
172        public_key: PublicKey,
173        subject: String,
174        resource: String,
175        operation: String,
176    ) -> Result<Self, TokenError> {
177        // Convert bytes to base64 for internal storage
178        let biscuit = Biscuit::from(&token, public_key)?;
179        let token_string = biscuit
180            .to_base64()
181            .map_err(|e| TokenError::generic(format!("Failed to encode token: {e}")))?;
182        Ok(Self {
183            token: token_string,
184            public_key,
185            mode: VerificationMode::Identity { subject },
186            resource,
187            operation,
188            context: VerificationContext::default(),
189            service_chain: None,
190        })
191    }
192
193    /// Creates a new capability-based verifier (no subject required).
194    ///
195    /// This verifier will accept any token that grants the specified capability
196    /// (resource + operation), regardless of the subject. The subject is derived
197    /// from the token's rights instead of being provided explicitly.
198    ///
199    /// # Arguments
200    /// * `token` - The base64-encoded authorization token to verify
201    /// * `public_key` - The public key used to verify the token signature
202    /// * `resource` - The resource identifier to verify authorization against
203    /// * `operation` - The operation to verify authorization for
204    pub fn new_capability(
205        token: String,
206        public_key: PublicKey,
207        resource: String,
208        operation: String,
209    ) -> Self {
210        Self {
211            token,
212            public_key,
213            mode: VerificationMode::Capability,
214            resource,
215            operation,
216            context: VerificationContext::default(),
217            service_chain: None,
218        }
219    }
220
221    /// Creates a new capability-based verifier from raw token bytes.
222    ///
223    /// This is the binary token version of `new_capability`. It accepts any token
224    /// that grants the specified capability (resource + operation), regardless of
225    /// the subject.
226    ///
227    /// # Arguments
228    /// * `token` - The raw binary Biscuit token bytes
229    /// * `public_key` - The public key used to verify the token signature
230    /// * `resource` - The resource identifier to verify authorization against
231    /// * `operation` - The operation to verify authorization for
232    pub fn from_bytes_capability(
233        token: Vec<u8>,
234        public_key: PublicKey,
235        resource: String,
236        operation: String,
237    ) -> Result<Self, TokenError> {
238        let biscuit = Biscuit::from(&token, public_key)?;
239        let token_string = biscuit
240            .to_base64()
241            .map_err(|e| TokenError::generic(format!("Failed to encode token: {e}")))?;
242        Ok(Self {
243            token: token_string,
244            public_key,
245            mode: VerificationMode::Capability,
246            resource,
247            operation,
248            context: VerificationContext::default(),
249            service_chain: None,
250        })
251    }
252
253    /// Adds a domain restriction to the verification.
254    ///
255    /// When set, adds a domain fact to the authorizer. This is required for
256    /// verifying domain-restricted tokens.
257    ///
258    /// # Arguments
259    /// * `domain` - The domain to verify against (e.g., "example.com")
260    pub fn with_domain(mut self, domain: String) -> Self {
261        self.context.domain = Some(domain);
262        self
263    }
264
265    /// Adds a prefix restriction to the verification.
266    ///
267    /// When set, adds a prefix fact to the authorizer. This is required for
268    /// verifying prefix-restricted tokens.
269    ///
270    /// # Arguments
271    /// * `prefix` - The prefix to verify against (e.g., "tenant/TENANTID/user/USERID/")
272    pub fn with_prefix(mut self, prefix: String) -> Self {
273        self.context.prefix = Some(prefix);
274        self
275    }
276
277    /// Adds service chain attestation verification.
278    ///
279    /// When set, verifies that the token has been properly attested by the
280    /// specified service chain nodes.
281    ///
282    /// # Arguments
283    /// * `service_nodes` - The list of service nodes in the chain
284    /// * `component` - Optional specific component to verify in the chain
285    pub fn with_service_chain(
286        mut self,
287        service_nodes: Vec<ServiceNode>,
288        component: Option<String>,
289    ) -> Self {
290        self.service_chain = Some((service_nodes, component));
291        self
292    }
293
294    /// Performs the token verification with the configured parameters.
295    ///
296    /// # Returns
297    /// * `Ok(())` - If the token is valid and meets all verification requirements
298    /// * `Err(TokenError)` - If verification fails for any reason
299    ///
300    /// # Errors
301    /// Returns an error if:
302    /// - The token is malformed or cannot be parsed
303    /// - The token signature is invalid
304    /// - The token has expired
305    /// - The token does not grant the required access rights
306    /// - The domain doesn't match (if domain restriction is set on token)
307    /// - Service chain attestation fails (if service chain is configured)
308    pub fn verify(self) -> Result<(), TokenError> {
309        let biscuit = Biscuit::from_base64(&self.token, self.public_key)?;
310
311        if let Some((service_nodes, component)) = self.service_chain {
312            // Service chain verification
313            verify_raw_service_chain_biscuit(
314                biscuit,
315                self.mode,
316                self.resource,
317                self.operation,
318                service_nodes,
319                component,
320                self.context,
321            )
322        } else {
323            // Basic verification
324            verify_raw_biscuit(
325                biscuit,
326                self.mode,
327                self.resource,
328                self.operation,
329                self.context,
330            )
331        }
332    }
333}
334
335pub(crate) fn build_base_authorizer(
336    subject: String,
337    resource: String,
338    operation: String,
339    context: &VerificationContext,
340) -> Result<biscuit::AuthorizerBuilder, TokenError> {
341    let now = Utc::now().timestamp();
342
343    let mut authz = authorizer!(
344        r#"
345            time({now});
346            resource({resource});
347            subject({subject});
348            operation({operation});
349            allow if true;
350        "#
351    );
352
353    // Add domain fact if specified
354    if let Some(domain) = context.domain.clone() {
355        authz = authz.fact(fact!(r#"domain({domain});"#))?;
356    }
357
358    // Add prefix fact if specified
359    if let Some(prefix) = context.prefix.clone() {
360        authz = authz.fact(fact!(r#"prefix({prefix});"#))?;
361    }
362
363    Ok(authz)
364}
365
366/// Build a capability-based authorizer that derives the subject from the token's rights.
367///
368/// Unlike `build_base_authorizer`, this function does not require a subject parameter.
369/// Instead, it uses a Datalog rule to derive the subject from any `right(subject, resource, operation)`
370/// facts present in the token. This allows verification based solely on capability (resource + operation)
371/// without needing to know the identity.
372pub(crate) fn build_capability_authorizer(
373    resource: String,
374    operation: String,
375    context: &VerificationContext,
376) -> Result<biscuit::AuthorizerBuilder, TokenError> {
377    let now = Utc::now().timestamp();
378
379    let mut authz = authorizer!(
380        r#"
381            time({now});
382            resource({resource});
383            operation({operation});
384            // Derive subject from the token's rights instead of providing it explicitly
385            subject($sub) <- right($sub, {resource}, {operation});
386            allow if true;
387        "#
388    );
389
390    // Add domain fact if specified
391    if let Some(domain) = context.domain.clone() {
392        authz = authz.fact(fact!(r#"domain({domain});"#))?;
393    }
394
395    // Add prefix fact if specified
396    if let Some(prefix) = context.prefix.clone() {
397        authz = authz.fact(fact!(r#"prefix({prefix});"#))?;
398    }
399
400    Ok(authz)
401}
402
403fn verify_raw_biscuit(
404    biscuit: Biscuit,
405    mode: VerificationMode,
406    resource: String,
407    operation: String,
408    context: VerificationContext,
409) -> Result<(), TokenError> {
410    let authz = match &mode {
411        VerificationMode::Identity { subject } => build_base_authorizer(
412            subject.clone(),
413            resource.clone(),
414            operation.clone(),
415            &context,
416        )?,
417        VerificationMode::Capability => {
418            build_capability_authorizer(resource.clone(), operation.clone(), &context)?
419        }
420    };
421
422    match authz.build(&biscuit)?.authorize() {
423        Ok(_) => Ok(()),
424        Err(e) => Err(convert_authorization_error(
425            e,
426            mode.subject(),
427            Some(&resource),
428            Some(&operation),
429            &context,
430        )),
431    }
432}
433
434/// Verifies a Biscuit authorization token locally without contacting the authorization server.
435///
436/// This function performs local verification of a Biscuit token using the provided public key.
437/// It validates that the token grants access to the specified resource for the given subject.
438///
439/// # Arguments
440///
441/// * `token` - The binary Biscuit token bytes (typically decoded from Base64)
442/// * `public_key` - The public key used to verify the token signature
443/// * `subject` - The subject (user) identifier to verify authorization for
444/// * `resource` - The resource identifier to verify authorization against
445/// * `operation` - The operation to verify authorization for
446///
447/// # Returns
448///
449/// * `Ok(())` - If the token is valid and grants access to the resource
450/// * `Err(TokenError)` - If verification fails for any reason
451///
452/// # Errors
453///
454/// Returns an error if:
455/// - The token is malformed or cannot be parsed
456/// - The token signature is invalid
457/// - The token does not grant the required access rights
458/// - The token has expired or other authorization checks fail
459pub fn verify_biscuit_local(
460    token: Vec<u8>,
461    public_key: PublicKey,
462    subject: String,
463    resource: String,
464    operation: String,
465) -> Result<(), TokenError> {
466    AuthorizationVerifier::from_bytes(token, public_key, subject, resource, operation)?.verify()
467}
468
469/// Verifies a Biscuit authorization token locally without contacting the authorization server.
470///
471/// This function performs local verification of a Biscuit token using the provided public key.
472/// It validates that the token grants access to the specified resource for the given subject.
473///
474/// # Arguments
475///
476/// * `token` - The base64-encoded Biscuit token string
477/// * `public_key` - The public key used to verify the token signature
478/// * `subject` - The subject (user) identifier to verify authorization for
479/// * `resource` - The resource identifier to verify authorization against
480/// * `operation` - The operation to verify authorization for
481///
482/// # Returns
483///
484/// * `Ok(())` - If the token is valid and grants access to the resource
485/// * `Err(TokenError)` - If verification fails for any reason
486///
487/// # Errors
488///
489/// Returns an error if:
490/// - The token is malformed or cannot be parsed
491/// - The token signature is invalid
492/// - The token does not grant the required access rights
493/// - The token has expired or other authorization checks fail
494pub fn verify_token_local(
495    token: &str,
496    public_key: PublicKey,
497    subject: &str,
498    resource: &str,
499    operation: &str,
500) -> Result<(), TokenError> {
501    AuthorizationVerifier::new(
502        token.to_string(),
503        public_key,
504        subject.to_string(),
505        resource.to_string(),
506        operation.to_string(),
507    )
508    .verify()
509}
510
511/// Takes a public key encoded as a string in the format "ed25519/..." or "secp256r1/..."
512/// and returns a PublicKey.
513pub fn biscuit_key_from_string(key: String) -> Result<PublicKey, TokenError> {
514    let parts = key.split('/').collect::<Vec<&str>>();
515    if parts.len() != 2 {
516        return Err(TokenError::invalid_key_format(
517            "Key must be in format 'algorithm/hexkey'",
518        ));
519    }
520
521    let alg = match parts[0] {
522        "ed25519" => Algorithm::Ed25519,
523        "secp256r1" => Algorithm::Secp256r1,
524        _ => {
525            return Err(TokenError::invalid_key_format(
526                "Unsupported algorithm, must be ed25519 or secp256r1",
527            ))
528        }
529    };
530
531    // decode the key from hex
532    let key_bytes = hex::decode(parts[1])?;
533
534    // construct the public key
535    let key = PublicKey::from_bytes(&key_bytes, alg)
536        .map_err(|e| TokenError::invalid_key_format(e.to_string()))?;
537
538    Ok(key)
539}
540
541fn verify_raw_service_chain_biscuit(
542    biscuit: Biscuit,
543    mode: VerificationMode,
544    resource: String,
545    operation: String,
546    service_nodes: Vec<ServiceNode>,
547    component: Option<String>,
548    context: VerificationContext,
549) -> Result<(), TokenError> {
550    let mut authz = match &mode {
551        VerificationMode::Identity { subject } => build_base_authorizer(
552            subject.clone(),
553            resource.clone(),
554            operation.clone(),
555            &context,
556        )?,
557        VerificationMode::Capability => {
558            build_capability_authorizer(resource.clone(), operation.clone(), &context)?
559        }
560    };
561
562    let mut component_found = false;
563    if component.is_none() {
564        component_found = true;
565    }
566    for service_node in &service_nodes {
567        if let Some(ref component) = component {
568            if component == &service_node.component {
569                component_found = true;
570                break;
571            }
572        }
573        let service = resource.clone();
574        let node_name = service_node.component.clone();
575        let node_key = biscuit_key_from_string(service_node.public_key.clone())?;
576        authz = authz.check(check!(
577            r#"
578                check if node({service}, {node_name}) trusting authority, {node_key};
579            "#
580        ))?;
581    }
582
583    if let Some(ref component_name) = component {
584        if !component_found {
585            return Err(TokenError::ServiceChainFailed {
586                component: component_name.clone(),
587                reason: ServiceChainFailure::ComponentNotFound,
588            });
589        }
590    }
591
592    // Service chain verification evaluates additional Datalog checks for each
593    // node in the chain. This can exceed default time limits on slower systems,
594    // so we use extended limits here (default is 1ms, we use 10ms).
595    let service_chain_limits = RunLimits {
596        max_time: Duration::from_millis(10),
597        ..Default::default()
598    };
599    match authz
600        .build(&biscuit)?
601        .authorize_with_limits(service_chain_limits)
602    {
603        Ok(_) => Ok(()),
604        Err(e) => Err(convert_service_chain_error(
605            e,
606            mode.subject_or_unknown(),
607            &resource,
608            &operation,
609            service_nodes,
610            &context,
611        )),
612    }
613}
614
615pub fn verify_service_chain_biscuit_local(
616    token: Vec<u8>,
617    public_key: PublicKey,
618    subject: String,
619    resource: String,
620    operation: String,
621    service_nodes: Vec<ServiceNode>,
622    component: Option<String>,
623) -> Result<(), TokenError> {
624    AuthorizationVerifier::from_bytes(token, public_key, subject, resource, operation)?
625        .with_service_chain(service_nodes, component)
626        .verify()
627}
628
629pub fn verify_service_chain_token_local(
630    token: &str,
631    public_key: PublicKey,
632    subject: &str,
633    resource: &str,
634    operation: &str,
635    service_nodes: Vec<ServiceNode>,
636    component: Option<String>,
637) -> Result<(), TokenError> {
638    AuthorizationVerifier::new(
639        token.to_string(),
640        public_key,
641        subject.to_string(),
642        resource.to_string(),
643        operation.to_string(),
644    )
645    .with_service_chain(service_nodes, component)
646    .verify()
647}
648
649/// Verifies a Biscuit authorization token based on capability (resource + operation) only.
650///
651/// This function performs capability-based verification without requiring a subject parameter.
652/// The subject is derived from the token's rights - any subject that has the specified right
653/// for the resource and operation will satisfy verification.
654///
655/// This is useful for services that only care about authorization for an action, not identity.
656/// For example, a telemetry service that only needs to verify write permission, not who is writing.
657///
658/// # Arguments
659///
660/// * `token` - The base64-encoded Biscuit token string
661/// * `public_key` - The public key used to verify the token signature
662/// * `resource` - The resource identifier to verify authorization against
663/// * `operation` - The operation to verify authorization for
664///
665/// # Returns
666///
667/// * `Ok(())` - If the token is valid and grants access to the resource+operation
668/// * `Err(TokenError)` - If verification fails
669///
670/// # Example
671///
672/// ```no_run
673/// use hessra_token_authz::{verify_capability_token_local, create_token};
674/// use hessra_token_core::{KeyPair, TokenError};
675///
676/// let keypair = KeyPair::new();
677/// let public_key = keypair.public();
678///
679/// // Create a token for alice to read resource1
680/// let token = create_token(
681///     "alice".to_string(),
682///     "resource1".to_string(),
683///     "read".to_string(),
684///     keypair,
685/// )
686/// .map_err(|e| TokenError::Generic(e.to_string()))?;
687///
688/// // Verify capability without caring about the subject
689/// verify_capability_token_local(&token, public_key, "resource1", "read")?;
690/// # Ok::<(), hessra_token_core::TokenError>(())
691/// ```
692pub fn verify_capability_token_local(
693    token: &str,
694    public_key: PublicKey,
695    resource: &str,
696    operation: &str,
697) -> Result<(), TokenError> {
698    AuthorizationVerifier::new_capability(
699        token.to_string(),
700        public_key,
701        resource.to_string(),
702        operation.to_string(),
703    )
704    .verify()
705}
706
707/// Verifies a Biscuit authorization token based on capability (resource + operation) only.
708///
709/// This is the binary token version of `verify_capability_token_local`.
710///
711/// # Arguments
712///
713/// * `token` - The binary Biscuit token bytes
714/// * `public_key` - The public key used to verify the token signature
715/// * `resource` - The resource identifier to verify authorization against
716/// * `operation` - The operation to verify authorization for
717///
718/// # Returns
719///
720/// * `Ok(())` - If the token is valid and grants access to the resource+operation
721/// * `Err(TokenError)` - If verification fails
722pub fn verify_capability_biscuit_local(
723    token: Vec<u8>,
724    public_key: PublicKey,
725    resource: String,
726    operation: String,
727) -> Result<(), TokenError> {
728    AuthorizationVerifier::from_bytes_capability(token, public_key, resource, operation)?.verify()
729}
730
731/// Verifies a service chain token based on capability without requiring subject.
732///
733/// This combines service chain attestation verification with capability-based verification.
734/// The token must have the required service chain attestations and grant the specified
735/// capability (resource + operation), but the subject is derived from the token rather
736/// than being provided explicitly.
737///
738/// # Arguments
739///
740/// * `token` - The base64-encoded Biscuit token string
741/// * `public_key` - The public key used to verify the token signature
742/// * `resource` - The resource identifier to verify authorization against
743/// * `operation` - The operation to verify authorization for
744/// * `service_nodes` - The list of service nodes in the chain
745/// * `component` - Optional specific component to verify in the chain
746///
747/// # Returns
748///
749/// * `Ok(())` - If the token is valid and grants access
750/// * `Err(TokenError)` - If verification fails
751pub fn verify_service_chain_capability_token_local(
752    token: &str,
753    public_key: PublicKey,
754    resource: &str,
755    operation: &str,
756    service_nodes: Vec<ServiceNode>,
757    component: Option<String>,
758) -> Result<(), TokenError> {
759    AuthorizationVerifier::new_capability(
760        token.to_string(),
761        public_key,
762        resource.to_string(),
763        operation.to_string(),
764    )
765    .with_service_chain(service_nodes, component)
766    .verify()
767}
768
769/// Binary version of `verify_service_chain_capability_token_local`.
770///
771/// Verifies a service chain token from raw bytes based on capability without requiring subject.
772///
773/// # Arguments
774///
775/// * `token` - The binary Biscuit token bytes
776/// * `public_key` - The public key used to verify the token signature
777/// * `resource` - The resource identifier to verify authorization against
778/// * `operation` - The operation to verify authorization for
779/// * `service_nodes` - The list of service nodes in the chain
780/// * `component` - Optional specific component to verify in the chain
781///
782/// # Returns
783///
784/// * `Ok(())` - If the token is valid and grants access
785/// * `Err(TokenError)` - If verification fails
786pub fn verify_service_chain_capability_biscuit_local(
787    token: Vec<u8>,
788    public_key: PublicKey,
789    resource: String,
790    operation: String,
791    service_nodes: Vec<ServiceNode>,
792    component: Option<String>,
793) -> Result<(), TokenError> {
794    AuthorizationVerifier::from_bytes_capability(token, public_key, resource, operation)?
795        .with_service_chain(service_nodes, component)
796        .verify()
797}
798
799/// Convert biscuit authorization errors to detailed authorization errors
800fn convert_authorization_error(
801    err: biscuit::error::Token,
802    subject: Option<&str>,
803    resource: Option<&str>,
804    operation: Option<&str>,
805    context: &VerificationContext,
806) -> TokenError {
807    use biscuit::error::{Logic, Token};
808
809    match err {
810        Token::FailedLogic(logic_err) => match &logic_err {
811            Logic::Unauthorized { checks, .. } | Logic::NoMatchingPolicy { checks } => {
812                // Try to parse each failed check for more specific errors
813                for failed_check in checks.iter() {
814                    let (block_id, check_id, rule) = match failed_check {
815                        biscuit::error::FailedCheck::Block(block_check) => (
816                            block_check.block_id,
817                            block_check.check_id,
818                            block_check.rule.clone(),
819                        ),
820                        biscuit::error::FailedCheck::Authorizer(auth_check) => {
821                            (0, auth_check.check_id, auth_check.rule.clone())
822                        }
823                    };
824
825                    // Try to parse the check for specific error types
826                    let parsed_error = parse_check_failure(block_id, check_id, &rule);
827
828                    // Enhance domain errors with context we have
829                    match parsed_error {
830                        TokenError::DomainMismatch {
831                            expected,
832                            block_id,
833                            check_id,
834                            ..
835                        } => {
836                            return TokenError::DomainMismatch {
837                                expected,
838                                provided: context.domain.clone(),
839                                block_id,
840                                check_id,
841                            };
842                        }
843                        TokenError::Expired { .. } => return parsed_error,
844                        _ => {}
845                    }
846                }
847
848                // Check if this looks like a rights denial (no matching policy)
849                if matches!(logic_err, Logic::NoMatchingPolicy { .. }) {
850                    return parse_authorization_failure(
851                        subject,
852                        resource,
853                        operation,
854                        &format!("{checks:?}"),
855                    );
856                }
857
858                // If we couldn't parse any specific error, use generic conversion
859                TokenError::from(Token::FailedLogic(logic_err))
860            }
861            other => TokenError::from(Token::FailedLogic(other.clone())),
862        },
863        other => TokenError::from(other),
864    }
865}
866
867/// Convert biscuit authorization errors to service chain specific errors
868fn convert_service_chain_error(
869    err: biscuit::error::Token,
870    subject: &str,
871    resource: &str,
872    operation: &str,
873    service_nodes: Vec<ServiceNode>,
874    context: &VerificationContext,
875) -> TokenError {
876    use biscuit::error::{Logic, Token};
877
878    match err {
879        Token::FailedLogic(logic_err) => match &logic_err {
880            Logic::Unauthorized { checks, .. } | Logic::NoMatchingPolicy { checks } => {
881                // Check if any failure is service chain related
882                for failed_check in checks.iter() {
883                    let (block_id, check_id, rule) = match failed_check {
884                        biscuit::error::FailedCheck::Block(block_check) => (
885                            block_check.block_id,
886                            block_check.check_id,
887                            block_check.rule.clone(),
888                        ),
889                        biscuit::error::FailedCheck::Authorizer(auth_check) => {
890                            (0, auth_check.check_id, auth_check.rule.clone())
891                        }
892                    };
893
894                    // Check if this is a service chain check (contains "node(")
895                    if rule.contains("node(") {
896                        // Try to extract component name from the rule
897                        for service_node in &service_nodes {
898                            if rule.contains(&service_node.component) {
899                                return TokenError::ServiceChainFailed {
900                                    component: service_node.component.clone(),
901                                    reason: ServiceChainFailure::MissingAttestation,
902                                };
903                            }
904                        }
905
906                        // Generic service chain failure
907                        return TokenError::ServiceChainFailed {
908                            component: resource.to_string(),
909                            reason: ServiceChainFailure::Other(
910                                "Service chain attestation check failed".to_string(),
911                            ),
912                        };
913                    }
914
915                    // Check for other specific error types
916                    let parsed_error = parse_check_failure(block_id, check_id, &rule);
917                    match parsed_error {
918                        TokenError::DomainMismatch {
919                            expected,
920                            block_id,
921                            check_id,
922                            ..
923                        } => {
924                            return TokenError::DomainMismatch {
925                                expected,
926                                provided: context.domain.clone(),
927                                block_id,
928                                check_id,
929                            };
930                        }
931                        TokenError::Expired { .. } => return parsed_error,
932                        _ => {}
933                    }
934                }
935
936                // Fallback to authorization error
937                parse_authorization_failure(
938                    Some(subject),
939                    Some(resource),
940                    Some(operation),
941                    &format!("{checks:?}"),
942                )
943            }
944            other => TokenError::from(Token::FailedLogic(other.clone())),
945        },
946        other => TokenError::from(other),
947    }
948}