openauth_plugins/api_key/routes/
update.rs1use http::{Method, StatusCode};
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use serde_json::Value;
4use time::OffsetDateTime;
5
6use super::{
7 body, config_id_matches, current_identity, endpoint, error, future_expiration, json,
8 metadata_is_object, SharedConfigurations,
9};
10use crate::api_key::errors;
11use crate::api_key::models::ApiKeyRecord;
12use crate::api_key::options::{ApiKeyPermissions, ApiKeyReference};
13use crate::api_key::organization::{ensure_organization_permission, owns_user_key, ApiKeyAction};
14use crate::api_key::storage::ApiKeyStore;
15
16#[derive(Debug, Clone, Deserialize, Serialize, Default)]
17#[serde(rename_all = "camelCase")]
18pub struct UpdateApiKeyRequest {
19 pub key_id: String,
20 pub config_id: Option<String>,
21 pub user_id: Option<String>,
22 pub name: Option<String>,
23 pub enabled: Option<bool>,
24 pub remaining: Option<i64>,
25 pub refill_amount: Option<i64>,
26 pub refill_interval: Option<i64>,
27 pub metadata: Option<Value>,
28 #[serde(default, skip_serializing_if = "UpdateField::is_missing")]
29 pub expires_in: UpdateField<i64>,
30 pub rate_limit_enabled: Option<bool>,
31 pub rate_limit_time_window: Option<i64>,
32 pub rate_limit_max: Option<i64>,
33 #[serde(default, skip_serializing_if = "UpdateField::is_missing")]
34 pub permissions: UpdateField<ApiKeyPermissions>,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Default)]
38pub enum UpdateField<T> {
39 #[default]
40 Missing,
41 Null,
42 Value(T),
43}
44
45impl<T> UpdateField<T> {
46 fn is_missing(&self) -> bool {
47 matches!(self, Self::Missing)
48 }
49}
50
51impl<'de, T> Deserialize<'de> for UpdateField<T>
52where
53 T: Deserialize<'de>,
54{
55 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
56 where
57 D: Deserializer<'de>,
58 {
59 Option::<T>::deserialize(deserializer)
60 .map(|value| value.map(Self::Value).unwrap_or(Self::Null))
61 }
62}
63
64impl<T> Serialize for UpdateField<T>
65where
66 T: Serialize,
67{
68 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
69 where
70 S: Serializer,
71 {
72 match self {
73 Self::Missing | Self::Null => serializer.serialize_none(),
74 Self::Value(value) => value.serialize(serializer),
75 }
76 }
77}
78
79pub fn update_endpoint(
80 configurations: SharedConfigurations,
81) -> openauth_core::api::AsyncAuthEndpoint {
82 endpoint(
83 "/api-key/update",
84 Method::POST,
85 configurations,
86 |context, request, configurations| {
87 Box::pin(async move {
88 let input: UpdateApiKeyRequest = body(&request)?;
89 let options = configurations.resolve(input.config_id.as_deref())?;
90 let identity = current_identity(context, &request).await?;
91 let actor_user_id = match &identity {
92 Some(identity) => {
93 if input
94 .user_id
95 .as_deref()
96 .is_some_and(|user_id| user_id != identity.user.id)
97 {
98 return error(StatusCode::UNAUTHORIZED, errors::UNAUTHORIZED_SESSION);
99 }
100 identity.user.id.clone()
101 }
102 None => {
103 let Some(user_id) = input.user_id.clone() else {
104 return error(StatusCode::UNAUTHORIZED, errors::UNAUTHORIZED_SESSION);
105 };
106 user_id
107 }
108 };
109 let store = ApiKeyStore::new(context, &options);
110 let Some(mut api_key) = store.get_by_id(&input.key_id).await? else {
111 return error(StatusCode::NOT_FOUND, errors::KEY_NOT_FOUND);
112 };
113 let expected_config_id = options.config_id.as_deref().unwrap_or("default");
114 if !config_id_matches(&api_key.config_id, expected_config_id) {
115 return error(StatusCode::NOT_FOUND, errors::KEY_NOT_FOUND);
116 }
117 match options.reference {
118 ApiKeyReference::User
119 if owns_user_key(
120 options.reference,
121 &api_key.reference_id,
122 &actor_user_id,
123 ) => {}
124 ApiKeyReference::User => {
125 return error(StatusCode::NOT_FOUND, errors::KEY_NOT_FOUND)
126 }
127 ApiKeyReference::Organization => {
128 if let Err(error) = ensure_organization_permission(
129 context,
130 &actor_user_id,
131 &api_key.reference_id,
132 ApiKeyAction::Update,
133 )
134 .await
135 {
136 return error_response_from_openauth(error);
137 }
138 }
139 }
140 let has_cookie = request.headers().contains_key(http::header::COOKIE);
141 if has_cookie && has_server_only_update(&input) {
142 return error(StatusCode::BAD_REQUEST, errors::SERVER_ONLY_PROPERTY);
143 }
144 if no_values_to_update(&input, &options) {
145 return error(StatusCode::BAD_REQUEST, errors::NO_VALUES_TO_UPDATE);
146 }
147 if let Err(code) = apply_update(&mut api_key, input, &options) {
148 return error(StatusCode::BAD_REQUEST, code);
149 }
150 let Some(updated) = store.update(&api_key).await? else {
151 return error(
152 StatusCode::INTERNAL_SERVER_ERROR,
153 errors::FAILED_TO_UPDATE_API_KEY,
154 );
155 };
156 json(StatusCode::OK, &updated.public())
157 })
158 },
159 )
160}
161
162fn no_values_to_update(
163 input: &UpdateApiKeyRequest,
164 options: &crate::api_key::options::ApiKeyConfiguration,
165) -> bool {
166 input.name.is_none()
167 && input.enabled.is_none()
168 && input.remaining.is_none()
169 && input.refill_amount.is_none()
170 && input.refill_interval.is_none()
171 && (input.metadata.is_none() || !options.enable_metadata)
172 && input.expires_in.is_missing()
173 && input.rate_limit_enabled.is_none()
174 && input.rate_limit_time_window.is_none()
175 && input.rate_limit_max.is_none()
176 && input.permissions.is_missing()
177}
178
179fn has_server_only_update(input: &UpdateApiKeyRequest) -> bool {
180 input.remaining.is_some()
181 || input.refill_amount.is_some()
182 || input.refill_interval.is_some()
183 || input.rate_limit_enabled.is_some()
184 || input.rate_limit_time_window.is_some()
185 || input.rate_limit_max.is_some()
186 || !input.permissions.is_missing()
187}
188
189fn apply_update(
190 api_key: &mut ApiKeyRecord,
191 input: UpdateApiKeyRequest,
192 options: &crate::api_key::options::ApiKeyConfiguration,
193) -> Result<(), &'static str> {
194 if let Some(name) = input.name {
195 if name.len() < options.minimum_name_length || name.len() > options.maximum_name_length {
196 return Err(errors::INVALID_NAME_LENGTH);
197 }
198 api_key.name = Some(name);
199 }
200 if let Some(metadata) = input.metadata.filter(|_| options.enable_metadata) {
201 if !metadata_is_object(&Some(metadata.clone())) {
202 return Err(errors::INVALID_METADATA_TYPE);
203 }
204 api_key.metadata = Some(metadata);
205 }
206 if input.refill_amount.is_some() ^ input.refill_interval.is_some() {
207 return Err(errors::REFILL_AMOUNT_AND_INTERVAL_REQUIRED);
208 }
209 match input.expires_in {
210 UpdateField::Missing => {}
211 UpdateField::Null => {
212 if options.key_expiration.disable_custom_expires_time {
213 return Err(errors::KEY_DISABLED_EXPIRATION);
214 }
215 api_key.expires_at = None;
216 }
217 UpdateField::Value(expires_in) => {
218 if options.key_expiration.disable_custom_expires_time {
219 return Err(errors::KEY_DISABLED_EXPIRATION);
220 }
221 let days = expires_in / (60 * 60 * 24);
222 if days < options.key_expiration.min_expires_in_days {
223 return Err(errors::EXPIRES_IN_IS_TOO_SMALL);
224 }
225 if days > options.key_expiration.max_expires_in_days {
226 return Err(errors::EXPIRES_IN_IS_TOO_LARGE);
227 }
228 api_key.expires_at = future_expiration(Some(expires_in));
229 }
230 }
231 if let Some(enabled) = input.enabled {
232 api_key.enabled = enabled;
233 }
234 if input.remaining.is_some() {
235 api_key.remaining = input.remaining;
236 }
237 if input.refill_amount.is_some() {
238 api_key.refill_amount = input.refill_amount;
239 api_key.refill_interval = input.refill_interval;
240 }
241 if let Some(enabled) = input.rate_limit_enabled {
242 api_key.rate_limit_enabled = enabled;
243 }
244 if input.rate_limit_time_window.is_some() {
245 api_key.rate_limit_time_window = input.rate_limit_time_window;
246 }
247 if input.rate_limit_max.is_some() {
248 api_key.rate_limit_max = input.rate_limit_max;
249 }
250 match input.permissions {
251 UpdateField::Missing => {}
252 UpdateField::Null => api_key.permissions = None,
253 UpdateField::Value(permissions) => api_key.permissions = Some(permissions),
254 }
255 api_key.updated_at = OffsetDateTime::now_utc();
256 Ok(())
257}
258
259fn error_response_from_openauth(
260 error: openauth_core::error::OpenAuthError,
261) -> Result<openauth_core::api::ApiResponse, openauth_core::error::OpenAuthError> {
262 let message = error.to_string();
263 if message.contains(errors::message(errors::USER_NOT_MEMBER_OF_ORGANIZATION)) {
264 return super::error(
265 StatusCode::FORBIDDEN,
266 errors::USER_NOT_MEMBER_OF_ORGANIZATION,
267 );
268 }
269 if message.contains(errors::message(errors::ORGANIZATION_PLUGIN_REQUIRED)) {
270 return super::error(
271 StatusCode::INTERNAL_SERVER_ERROR,
272 errors::ORGANIZATION_PLUGIN_REQUIRED,
273 );
274 }
275 super::error(
276 StatusCode::FORBIDDEN,
277 errors::INSUFFICIENT_API_KEY_PERMISSIONS,
278 )
279}