siderite_core/security/
api_key.rs1use super::document_scheme;
4use crate::error::ApiError;
5use crate::extract::FromRequestParts;
6use crate::header::Cookies;
7use http::StatusCode;
8use http::request::Parts;
9use serde_json::json;
10use siderite_openapi::{Operation, SchemaRegistry};
11use std::fmt;
12use std::marker::PhantomData;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ApiKeyLocation {
17 Header,
19 Query,
21 Cookie,
23}
24
25impl ApiKeyLocation {
26 const fn openapi(self) -> &'static str {
28 match self {
29 Self::Header => "header",
30 Self::Query => "query",
31 Self::Cookie => "cookie",
32 }
33 }
34}
35
36pub trait ApiKeySpec: Send + Sync + 'static {
38 const NAME: &'static str;
40 const LOCATION: ApiKeyLocation;
42 const SCHEME: &'static str;
44}
45
46pub struct ApiKey<S: ApiKeySpec> {
53 pub key: String,
55 spec: PhantomData<fn() -> S>,
56}
57
58impl<S: ApiKeySpec> ApiKey<S> {
59 #[must_use]
61 pub fn new(key: impl Into<String>) -> Self {
62 Self {
63 key: key.into(),
64 spec: PhantomData,
65 }
66 }
67}
68
69impl<S: ApiKeySpec> fmt::Debug for ApiKey<S> {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.debug_struct("ApiKey")
72 .field("scheme", &S::SCHEME)
73 .field("key", &"[REDACTED]")
74 .finish()
75 }
76}
77
78impl<S: ApiKeySpec> Clone for ApiKey<S> {
79 fn clone(&self) -> Self {
80 Self::new(self.key.clone())
81 }
82}
83
84fn query_value(parts: &Parts, name: &str) -> Option<String> {
86 let pairs = serde_urlencoded::from_str::<Vec<(String, String)>>(parts.uri.query()?).ok()?;
87 pairs
88 .into_iter()
89 .find(|(key, value)| key == name && !value.is_empty())
90 .map(|(_, value)| value)
91}
92
93fn header_value(parts: &Parts, name: &str) -> Option<String> {
95 parts
96 .headers
97 .get_all(name)
98 .iter()
99 .filter_map(|value| value.to_str().ok())
100 .map(str::trim)
101 .find(|value| !value.is_empty())
102 .map(str::to_owned)
103}
104
105impl<S: ApiKeySpec> FromRequestParts for ApiKey<S> {
106 async fn from_request_parts(parts: &mut Parts) -> Result<Self, ApiError> {
107 let key = match S::LOCATION {
108 ApiKeyLocation::Header => header_value(parts, S::NAME),
109 ApiKeyLocation::Query => query_value(parts, S::NAME),
110 ApiKeyLocation::Cookie => {
111 let cookies = Cookies::from_request_parts(parts).await?;
112 cookies
113 .get(S::NAME)
114 .filter(|value| !value.is_empty())
115 .map(str::to_owned)
116 }
117 };
118 key.map(Self::new)
119 .ok_or_else(|| ApiError::new(StatusCode::UNAUTHORIZED, "Not authenticated.").absent())
120 }
121
122 fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
123 document_scheme(
124 op,
125 registry,
126 S::SCHEME,
127 json!({"type": "apiKey", "in": S::LOCATION.openapi(), "name": S::NAME}),
128 &[],
129 );
130 }
131}