canic_core/api/
auth.rs

1use crate::{
2    api::access::auth::AuthAccessApi,
3    cdk::types::Principal,
4    dto::{
5        auth::{DelegationAdminCommand, DelegationAdminResponse, DelegationCert, DelegationProof},
6        error::Error,
7    },
8    error::InternalErrorClass,
9    ops::{
10        auth::DelegatedTokenOps, config::ConfigOps, ic::IcOps, runtime::env::EnvOps,
11        storage::auth::DelegationStateOps,
12    },
13    workflow::auth::DelegationWorkflow,
14};
15use std::{sync::Arc, time::Duration};
16
17///
18/// DelegationApi
19///
20/// Requires delegation.enabled = true in config.
21///
22
23pub struct DelegationApi;
24
25impl DelegationApi {
26    fn map_delegation_error(err: crate::InternalError) -> Error {
27        match err.class() {
28            InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
29                Error::internal(err.to_string())
30            }
31            _ => Error::from(err),
32        }
33    }
34
35    pub fn prepare_issue(cert: DelegationCert) -> Result<(), Error> {
36        let cfg = ConfigOps::delegation_config().map_err(Error::from)?;
37        if !cfg.enabled {
38            return Err(Error::forbidden("delegation disabled"));
39        }
40
41        // Update-only step for certified delegation signatures.
42        DelegationWorkflow::prepare_delegation(&cert).map_err(Self::map_delegation_error)
43    }
44
45    pub fn get_issue(cert: DelegationCert) -> Result<DelegationProof, Error> {
46        let cfg = ConfigOps::delegation_config().map_err(Error::from)?;
47        if !cfg.enabled {
48            return Err(Error::forbidden("delegation disabled"));
49        }
50
51        // Query-only step; requires a data certificate in the query context.
52        DelegationWorkflow::get_delegation(cert).map_err(Self::map_delegation_error)
53    }
54
55    pub fn issue_and_store(cert: DelegationCert) -> Result<DelegationProof, Error> {
56        let cfg = ConfigOps::delegation_config().map_err(Error::from)?;
57        if !cfg.enabled {
58            return Err(Error::forbidden("delegation disabled"));
59        }
60
61        DelegationWorkflow::issue_and_store(cert).map_err(Self::map_delegation_error)
62    }
63
64    pub fn store_proof(proof: DelegationProof) -> Result<(), Error> {
65        let cfg = ConfigOps::delegation_config().map_err(Error::from)?;
66        if !cfg.enabled {
67            return Err(Error::forbidden("delegation disabled"));
68        }
69
70        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
71        let caller = IcOps::msg_caller();
72        if caller != root_pid {
73            return Err(Error::forbidden(
74                "delegation proof store requires root caller",
75            ));
76        }
77
78        DelegatedTokenOps::verify_delegation_proof(&proof, root_pid)
79            .map_err(Self::map_delegation_error)?;
80
81        DelegationStateOps::set_proof_from_dto(proof);
82        Ok(())
83    }
84
85    pub fn require_proof() -> Result<DelegationProof, Error> {
86        let cfg = ConfigOps::delegation_config().map_err(Error::from)?;
87        if !cfg.enabled {
88            return Err(Error::forbidden("delegation disabled"));
89        }
90
91        DelegationStateOps::proof_dto().ok_or_else(|| Error::not_found("delegation proof not set"))
92    }
93}
94
95///
96/// DelegationAdminApi
97///
98/// Admin façade for delegation rotation control.
99///
100
101pub struct DelegationAdminApi;
102
103impl DelegationAdminApi {
104    pub async fn admin(cmd: DelegationAdminCommand) -> Result<DelegationAdminResponse, Error> {
105        match cmd {
106            DelegationAdminCommand::StartRotation { interval_secs } => {
107                let started = Self::start_rotation(interval_secs).await?;
108                Ok(if started {
109                    DelegationAdminResponse::RotationStarted
110                } else {
111                    DelegationAdminResponse::RotationAlreadyRunning
112                })
113            }
114            DelegationAdminCommand::StopRotation => {
115                let stopped = Self::stop_rotation().await?;
116                Ok(if stopped {
117                    DelegationAdminResponse::RotationStopped
118                } else {
119                    DelegationAdminResponse::RotationNotRunning
120                })
121            }
122        }
123    }
124
125    pub async fn start_rotation(interval_secs: u64) -> Result<bool, Error> {
126        AuthAccessApi::caller_is_root(IcOps::msg_caller()).await?;
127
128        let cfg = ConfigOps::delegation_config().map_err(Error::from)?;
129        if !cfg.enabled {
130            return Err(Error::forbidden("delegation disabled"));
131        }
132
133        if interval_secs == 0 {
134            return Err(Error::invalid(
135                "rotation interval must be greater than zero",
136            ));
137        }
138
139        let template = rotation_template()?;
140        let template = Arc::new(template);
141        let interval = Duration::from_secs(interval_secs);
142
143        let started = DelegationWorkflow::start_rotation(
144            interval,
145            Arc::new({
146                let template = Arc::clone(&template);
147                move || {
148                    let now_secs = IcOps::now_secs();
149                    Ok(build_rotation_cert(template.as_ref(), now_secs))
150                }
151            }),
152            Arc::new(|proof| {
153                DelegationStateOps::set_proof_from_dto(proof);
154                Ok(())
155            }),
156        );
157
158        Ok(started)
159    }
160
161    pub async fn stop_rotation() -> Result<bool, Error> {
162        AuthAccessApi::caller_is_root(IcOps::msg_caller()).await?;
163        Ok(DelegationWorkflow::stop_rotation())
164    }
165}
166
167///
168/// DelegationRotationTemplate
169///
170
171struct DelegationRotationTemplate {
172    v: u16,
173    signer_pid: Principal,
174    audiences: Vec<String>,
175    scopes: Vec<String>,
176    ttl_secs: u64,
177}
178
179fn rotation_template() -> Result<DelegationRotationTemplate, Error> {
180    let proof = DelegationStateOps::proof_dto()
181        .ok_or_else(|| Error::not_found("delegation proof not set"))?;
182    let cert = proof.cert;
183
184    if cert.expires_at <= cert.issued_at {
185        return Err(Error::invalid(
186            "delegation cert expires_at must be greater than issued_at",
187        ));
188    }
189
190    let ttl_secs = cert.expires_at - cert.issued_at;
191
192    Ok(DelegationRotationTemplate {
193        v: cert.v,
194        signer_pid: cert.signer_pid,
195        audiences: cert.audiences,
196        scopes: cert.scopes,
197        ttl_secs,
198    })
199}
200
201fn build_rotation_cert(template: &DelegationRotationTemplate, now_secs: u64) -> DelegationCert {
202    DelegationCert {
203        v: template.v,
204        signer_pid: template.signer_pid,
205        audiences: template.audiences.clone(),
206        scopes: template.scopes.clone(),
207        issued_at: now_secs,
208        expires_at: now_secs.saturating_add(template.ttl_secs),
209    }
210}