Skip to main content

rustauth_plugins/api_key/routes/
update.rs

1use 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, request_is_external, 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) -> rustauth_core::api::AsyncAuthEndpoint {
82    endpoint(
83        "/api-key/update",
84        Method::POST,
85        configurations,
86        |context, request, configurations| async move {
87            let input: UpdateApiKeyRequest = body(&request)?;
88            let options = configurations.resolve(input.config_id.as_deref())?;
89            let identity = current_identity(&context, &request).await?;
90            let is_external = request_is_external();
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                // Only trusted server-side callers may name the actor explicitly.
103                None if !is_external => {
104                    let Some(user_id) = input.user_id.clone() else {
105                        return error(StatusCode::UNAUTHORIZED, errors::UNAUTHORIZED_SESSION);
106                    };
107                    user_id
108                }
109                None => {
110                    return error(StatusCode::UNAUTHORIZED, errors::UNAUTHORIZED_SESSION);
111                }
112            };
113            let store = ApiKeyStore::new(&context, &options);
114            let Some(mut api_key) = store.get_by_id(&input.key_id).await? else {
115                return error(StatusCode::NOT_FOUND, errors::KEY_NOT_FOUND);
116            };
117            let expected_config_id = options.config_id.as_deref().unwrap_or("default");
118            if !config_id_matches(&api_key.config_id, expected_config_id) {
119                return error(StatusCode::NOT_FOUND, errors::KEY_NOT_FOUND);
120            }
121            match options.reference {
122                ApiKeyReference::User
123                    if owns_user_key(options.reference, &api_key.reference_id, &actor_user_id) => {}
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_rustauth(error);
137                    }
138                }
139            }
140            if is_external && has_server_only_update(&input) {
141                return error(StatusCode::BAD_REQUEST, errors::SERVER_ONLY_PROPERTY);
142            }
143            if no_values_to_update(&input, &options) {
144                return error(StatusCode::BAD_REQUEST, errors::NO_VALUES_TO_UPDATE);
145            }
146            if let Err(code) = apply_update(&mut api_key, input, &options) {
147                return error(StatusCode::BAD_REQUEST, code);
148            }
149            let Some(mut updated) = store.update(&api_key).await? else {
150                return error(
151                    StatusCode::INTERNAL_SERVER_ERROR,
152                    errors::FAILED_TO_UPDATE_API_KEY,
153                );
154            };
155            store.migrate_metadata_if_needed(&mut updated).await;
156            json(StatusCode::OK, &updated.public())
157        },
158    )
159}
160
161fn no_values_to_update(
162    input: &UpdateApiKeyRequest,
163    options: &crate::api_key::options::ApiKeyConfiguration,
164) -> bool {
165    input.name.is_none()
166        && input.enabled.is_none()
167        && input.remaining.is_none()
168        && input.refill_amount.is_none()
169        && input.refill_interval.is_none()
170        && (input.metadata.is_none() || !options.enable_metadata)
171        && input.expires_in.is_missing()
172        && input.rate_limit_enabled.is_none()
173        && input.rate_limit_time_window.is_none()
174        && input.rate_limit_max.is_none()
175        && input.permissions.is_missing()
176}
177
178fn has_server_only_update(input: &UpdateApiKeyRequest) -> bool {
179    input.remaining.is_some()
180        || input.refill_amount.is_some()
181        || input.refill_interval.is_some()
182        || input.rate_limit_enabled.is_some()
183        || input.rate_limit_time_window.is_some()
184        || input.rate_limit_max.is_some()
185        || !input.permissions.is_missing()
186}
187
188fn apply_update(
189    api_key: &mut ApiKeyRecord,
190    input: UpdateApiKeyRequest,
191    options: &crate::api_key::options::ApiKeyConfiguration,
192) -> Result<(), &'static str> {
193    if let Some(name) = input.name {
194        if name.len() < options.minimum_name_length || name.len() > options.maximum_name_length {
195            return Err(errors::INVALID_NAME_LENGTH);
196        }
197        api_key.name = Some(name);
198    }
199    if let Some(metadata) = input.metadata.filter(|_| options.enable_metadata) {
200        if !metadata_is_object(&Some(metadata.clone())) {
201            return Err(errors::INVALID_METADATA_TYPE);
202        }
203        api_key.metadata = Some(metadata);
204    }
205    if input.refill_amount.is_some() && input.refill_interval.is_none() {
206        return Err(errors::REFILL_AMOUNT_AND_INTERVAL_REQUIRED);
207    }
208    if input.refill_interval.is_some() && input.refill_amount.is_none() {
209        return Err(errors::REFILL_INTERVAL_AND_AMOUNT_REQUIRED);
210    }
211    match input.expires_in {
212        UpdateField::Missing => {}
213        UpdateField::Null => {
214            if options.key_expiration.disable_custom_expires_time {
215                return Err(errors::KEY_DISABLED_EXPIRATION);
216            }
217            api_key.expires_at = None;
218        }
219        UpdateField::Value(expires_in) => {
220            if options.key_expiration.disable_custom_expires_time {
221                return Err(errors::KEY_DISABLED_EXPIRATION);
222            }
223            let days = expires_in / (60 * 60 * 24);
224            if days < options.key_expiration.min_expires_in_days {
225                return Err(errors::EXPIRES_IN_IS_TOO_SMALL);
226            }
227            if days > options.key_expiration.max_expires_in_days {
228                return Err(errors::EXPIRES_IN_IS_TOO_LARGE);
229            }
230            api_key.expires_at = future_expiration(Some(expires_in));
231        }
232    }
233    if let Some(enabled) = input.enabled {
234        api_key.enabled = enabled;
235    }
236    if input.remaining.is_some() {
237        api_key.remaining = input.remaining;
238    }
239    if input.refill_amount.is_some() {
240        api_key.refill_amount = input.refill_amount;
241        api_key.refill_interval = input.refill_interval;
242    }
243    if let Some(enabled) = input.rate_limit_enabled {
244        api_key.rate_limit_enabled = enabled;
245    }
246    if input.rate_limit_time_window.is_some() {
247        api_key.rate_limit_time_window = input.rate_limit_time_window;
248    }
249    if input.rate_limit_max.is_some() {
250        api_key.rate_limit_max = input.rate_limit_max;
251    }
252    match input.permissions {
253        UpdateField::Missing => {}
254        UpdateField::Null => api_key.permissions = None,
255        UpdateField::Value(permissions) => api_key.permissions = Some(permissions),
256    }
257    api_key.updated_at = OffsetDateTime::now_utc();
258    Ok(())
259}
260
261fn error_response_from_rustauth(
262    error: rustauth_core::error::RustAuthError,
263) -> Result<rustauth_core::api::ApiResponse, rustauth_core::error::RustAuthError> {
264    let message = error.to_string();
265    if message.contains(errors::message(errors::USER_NOT_MEMBER_OF_ORGANIZATION)) {
266        return super::error(
267            StatusCode::FORBIDDEN,
268            errors::USER_NOT_MEMBER_OF_ORGANIZATION,
269        );
270    }
271    if message.contains(errors::message(errors::ORGANIZATION_PLUGIN_REQUIRED)) {
272        return super::error(
273            StatusCode::INTERNAL_SERVER_ERROR,
274            errors::ORGANIZATION_PLUGIN_REQUIRED,
275        );
276    }
277    super::error(
278        StatusCode::FORBIDDEN,
279        errors::INSUFFICIENT_API_KEY_PERMISSIONS,
280    )
281}