1use crate::{
2 cdk::types::Principal,
3 dto::{
4 auth::{
5 DelegatedToken, DelegatedTokenClaims, DelegationAdminCommand, DelegationAdminResponse,
6 DelegationCert, DelegationProof,
7 },
8 error::Error,
9 },
10 error::InternalErrorClass,
11 ops::{
12 auth::DelegatedTokenOps, config::ConfigOps, ic::IcOps, runtime::env::EnvOps,
13 storage::auth::DelegationStateOps,
14 },
15 workflow::auth::DelegationWorkflow,
16};
17use std::{sync::Arc, time::Duration};
18
19pub struct DelegationApi;
26
27impl DelegationApi {
28 const DELEGATED_TOKENS_DISABLED: &str =
29 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
30
31 fn map_delegation_error(err: crate::InternalError) -> Error {
32 match err.class() {
33 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
34 Error::internal(err.to_string())
35 }
36 _ => Error::from(err),
37 }
38 }
39
40 pub fn sign_delegation_cert(cert: DelegationCert) -> Result<DelegationProof, Error> {
42 DelegatedTokenOps::sign_delegation_cert(cert).map_err(Self::map_delegation_error)
43 }
44
45 pub fn verify_delegation_proof(
50 proof: &DelegationProof,
51 authority_pid: Principal,
52 ) -> Result<(), Error> {
53 DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
54 .map_err(Self::map_delegation_error)
55 }
56
57 pub fn sign_token(
58 token_version: u16,
59 claims: DelegatedTokenClaims,
60 proof: DelegationProof,
61 ) -> Result<DelegatedToken, Error> {
62 DelegatedTokenOps::sign_token(token_version, claims, proof)
63 .map_err(Self::map_delegation_error)
64 }
65
66 pub fn verify_token(
71 token: &DelegatedToken,
72 authority_pid: Principal,
73 now_secs: u64,
74 ) -> Result<(), Error> {
75 DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
76 .map(|_| ())
77 .map_err(Self::map_delegation_error)
78 }
79
80 pub fn verify_token_claims(
85 token: &DelegatedToken,
86 authority_pid: Principal,
87 now_secs: u64,
88 ) -> Result<DelegatedTokenClaims, Error> {
89 DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
90 .map(|verified| verified.claims)
91 .map_err(Self::map_delegation_error)
92 }
93
94 pub fn prepare_issue(cert: DelegationCert) -> Result<(), Error> {
95 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
96 if !cfg.enabled {
97 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
98 }
99
100 DelegationWorkflow::prepare_delegation(&cert).map_err(Self::map_delegation_error)
102 }
103
104 pub fn get_issue(cert: DelegationCert) -> Result<DelegationProof, Error> {
105 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
106 if !cfg.enabled {
107 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
108 }
109
110 DelegationWorkflow::get_delegation(cert).map_err(Self::map_delegation_error)
112 }
113
114 pub fn issue_and_store(cert: DelegationCert) -> Result<DelegationProof, Error> {
115 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
116 if !cfg.enabled {
117 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
118 }
119
120 DelegationWorkflow::issue_and_store(cert).map_err(Self::map_delegation_error)
121 }
122
123 pub fn store_proof(proof: DelegationProof) -> Result<(), Error> {
124 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
125 if !cfg.enabled {
126 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
127 }
128
129 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
130 let caller = IcOps::msg_caller();
131 if caller != root_pid {
132 return Err(Error::forbidden(
133 "delegation proof store requires root caller",
134 ));
135 }
136
137 DelegatedTokenOps::verify_delegation_proof(&proof, root_pid)
138 .map_err(Self::map_delegation_error)?;
139
140 DelegationStateOps::set_proof_from_dto(proof);
141 Ok(())
142 }
143
144 pub fn require_proof() -> Result<DelegationProof, Error> {
145 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
146 if !cfg.enabled {
147 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
148 }
149
150 DelegationStateOps::proof_dto().ok_or_else(|| Error::not_found("delegation proof not set"))
151 }
152}
153
154pub struct DelegationAdminApi;
161
162impl DelegationAdminApi {
163 pub async fn admin(cmd: DelegationAdminCommand) -> Result<DelegationAdminResponse, Error> {
164 match cmd {
165 DelegationAdminCommand::StartRotation { interval_secs } => {
166 let started = Self::start_rotation(interval_secs).await?;
167 Ok(if started {
168 DelegationAdminResponse::RotationStarted
169 } else {
170 DelegationAdminResponse::RotationAlreadyRunning
171 })
172 }
173 DelegationAdminCommand::StopRotation => {
174 let stopped = Self::stop_rotation().await?;
175 Ok(if stopped {
176 DelegationAdminResponse::RotationStopped
177 } else {
178 DelegationAdminResponse::RotationNotRunning
179 })
180 }
181 }
182 }
183
184 #[allow(clippy::unused_async)]
185 pub async fn start_rotation(interval_secs: u64) -> Result<bool, Error> {
186 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
187 if !cfg.enabled {
188 return Err(Error::forbidden(DelegationApi::DELEGATED_TOKENS_DISABLED));
189 }
190
191 if interval_secs == 0 {
192 return Err(Error::invalid(
193 "rotation interval must be greater than zero",
194 ));
195 }
196
197 let template = rotation_template()?;
198 let template = Arc::new(template);
199 let interval = Duration::from_secs(interval_secs);
200
201 let started = DelegationWorkflow::start_rotation(
202 interval,
203 Arc::new({
204 let template = Arc::clone(&template);
205 move || {
206 let now_secs = IcOps::now_secs();
207 Ok(build_rotation_cert(template.as_ref(), now_secs))
208 }
209 }),
210 Arc::new(|proof| {
211 DelegationStateOps::set_proof_from_dto(proof);
212 Ok(())
213 }),
214 );
215
216 Ok(started)
217 }
218
219 #[allow(clippy::unused_async)]
220 pub async fn stop_rotation() -> Result<bool, Error> {
221 Ok(DelegationWorkflow::stop_rotation())
222 }
223}
224
225struct DelegationRotationTemplate {
230 v: u16,
231 signer_pid: Principal,
232 audiences: Vec<String>,
233 scopes: Vec<String>,
234 ttl_secs: u64,
235}
236
237fn rotation_template() -> Result<DelegationRotationTemplate, Error> {
238 let proof = DelegationStateOps::proof_dto()
239 .ok_or_else(|| Error::not_found("delegation proof not set"))?;
240 let cert = proof.cert;
241
242 if cert.expires_at <= cert.issued_at {
243 return Err(Error::invalid(
244 "delegation cert expires_at must be greater than issued_at",
245 ));
246 }
247
248 let ttl_secs = cert.expires_at - cert.issued_at;
249
250 Ok(DelegationRotationTemplate {
251 v: cert.v,
252 signer_pid: cert.signer_pid,
253 audiences: cert.audiences,
254 scopes: cert.scopes,
255 ttl_secs,
256 })
257}
258
259fn build_rotation_cert(template: &DelegationRotationTemplate, now_secs: u64) -> DelegationCert {
260 DelegationCert {
261 v: template.v,
262 signer_pid: template.signer_pid,
263 audiences: template.audiences.clone(),
264 scopes: template.scopes.clone(),
265 issued_at: now_secs,
266 expires_at: now_secs.saturating_add(template.ttl_secs),
267 }
268}