Skip to main content

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