rskit_auth/apikey/
manager.rs1use chrono::{DateTime, Utc};
4use rskit_errors::{AppError, ErrorCode};
5
6use super::{GenerateResult, Hasher, Key, Store, split_key, validate};
7
8#[derive(Debug, Clone, Default)]
10pub struct KeySpec {
11 pub key_id: String,
13 pub owner_id: String,
15 pub name: String,
17 pub prefix: String,
19 pub scopes: Vec<String>,
21 pub expires_at: Option<DateTime<Utc>>,
23}
24
25pub struct Manager<S> {
27 pub(super) store: S,
28 pub(super) hasher: Hasher,
29}
30
31impl<S> Manager<S> {
32 #[must_use]
34 pub const fn new(store: S, hasher: Hasher) -> Self {
35 Self { store, hasher }
36 }
37
38 #[must_use]
40 pub const fn store(&self) -> &S {
41 &self.store
42 }
43
44 #[must_use]
46 pub const fn hasher(&self) -> &Hasher {
47 &self.hasher
48 }
49}
50
51impl<S: Store> Manager<S> {
52 pub async fn issue_key(&self, spec: KeySpec) -> Result<(GenerateResult, Key), AppError> {
54 let issued = self.hasher.generate_key(&spec.prefix)?;
55 let record = Key {
56 id: spec.key_id,
57 owner_id: spec.owner_id,
58 name: spec.name,
59 key_prefix: issued.key_prefix.clone(),
60 key_digest: issued.key_digest.clone(),
61 scopes: spec.scopes,
62 is_active: true,
63 expires_at: spec.expires_at,
64 grace_ends_at: None,
65 rotated_by_id: None,
66 last_used_at: None,
67 created_at: Utc::now(),
68 };
69 self.store.create(record.clone()).await?;
70 Ok((issued, record))
71 }
72
73 pub async fn validate_key_with_scopes(
75 &self,
76 plain_key: &str,
77 required_scopes: &[String],
78 ) -> Result<Key, AppError> {
79 let (key_prefix, _secret) = split_key(plain_key)?;
80 let candidates = self.store.list_by_prefix(&key_prefix).await?;
81
82 let mut matched: Option<Key> = None;
83 for candidate in candidates {
84 let digest_matches = self.hasher.compare(plain_key, &candidate.key_digest);
85 if digest_matches && matched.is_none() {
86 matched = Some(candidate);
87 }
88 }
89
90 let mut matched = matched.ok_or_else(AppError::invalid_token)?;
91 validate(&matched).map_err(|_| AppError::invalid_token())?;
92 if required_scopes
93 .iter()
94 .any(|scope| !matched.scopes.iter().any(|granted| granted == scope))
95 {
96 return Err(AppError::new(
97 ErrorCode::Forbidden,
98 String::from("insufficient API key scope"),
99 ));
100 }
101
102 let used_at = Utc::now();
103 self.store.update_last_used(&matched.id, used_at).await?;
104 matched.last_used_at = Some(used_at);
105 Ok(matched)
106 }
107}
108
109#[async_trait::async_trait]
110impl<S: Store> super::KeyValidator for Manager<S> {
111 async fn validate_key(&self, plain_key: &str) -> Result<Key, AppError> {
112 self.validate_key_with_scopes(plain_key, &[]).await
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::KeySpec;
119 use crate::apikey::test_support::manager;
120
121 #[tokio::test]
122 async fn issue_persists_key_and_validates_with_required_scopes() {
123 let manager = manager();
124 let (issued, record) = manager
125 .issue_key(KeySpec {
126 key_id: String::from("key-1"),
127 owner_id: String::from("user-1"),
128 name: String::from("primary"),
129 prefix: String::from("pk"),
130 scopes: vec![String::from("read")],
131 expires_at: None,
132 })
133 .await
134 .unwrap();
135 assert_eq!(record.key_prefix, "pk");
136
137 let validated = manager
138 .validate_key_with_scopes(&issued.plain_key, &[String::from("read")])
139 .await
140 .unwrap();
141 assert_eq!(validated.owner_id, "user-1");
142 assert!(validated.last_used_at.is_some());
143 }
144
145 #[tokio::test]
146 async fn validate_rejects_missing_required_scope() {
147 let manager = manager();
148 let (issued, _record) = manager
149 .issue_key(KeySpec {
150 key_id: String::from("key-1"),
151 owner_id: String::from("user-1"),
152 name: String::from("primary"),
153 prefix: String::from("pk"),
154 scopes: vec![String::from("read")],
155 expires_at: None,
156 })
157 .await
158 .unwrap();
159
160 let error = manager
161 .validate_key_with_scopes(&issued.plain_key, &[String::from("write")])
162 .await
163 .unwrap_err();
164 assert_eq!(error.code(), rskit_errors::ErrorCode::Forbidden);
165 }
166}