kanidmd_core/https/extractors/
mod.rs1use crate::https::ServerState;
2use axum::{
3 async_trait,
4 extract::{connect_info::Connected, FromRequestParts},
5 http::{header::AUTHORIZATION as AUTHORISATION, request::Parts, StatusCode},
6};
7use axum_extra::extract::cookie::CookieJar;
8use compact_jwt::JwsCompact;
9use kanidm_proto::internal::COOKIE_BEARER_TOKEN;
10use kanidmd_lib::prelude::{ClientAuthInfo, ClientCertInfo, Source};
11use std::net::{IpAddr, SocketAddr};
12use std::str::FromStr;
13
14pub use kanidmd_lib::idm::server::DomainInfoRead;
16
17pub struct VerifiedClientInformation(pub ClientAuthInfo);
18
19#[async_trait]
20impl FromRequestParts<ServerState> for VerifiedClientInformation {
21 type Rejection = (StatusCode, &'static str);
22
23 #[instrument(level = "debug", skip_all)]
25 async fn from_request_parts(
26 parts: &mut Parts,
27 state: &ServerState,
28 ) -> Result<Self, Self::Rejection> {
29 let ClientConnInfo {
30 connection_addr: _,
31 client_ip_addr,
32 client_cert,
33 } = parts.extensions.remove::<ClientConnInfo>().ok_or((
34 StatusCode::INTERNAL_SERVER_ERROR,
35 "request info contains invalid data",
36 ))?;
37
38 let (basic_authz, bearer_token) = if let Some(header) = parts.headers.get(AUTHORISATION) {
39 if let Some((authz_type, authz_data)) = header
40 .to_str()
41 .map_err(|err| {
42 warn!(?err, "Invalid authz header, ignoring");
43 })
44 .ok()
45 .and_then(|s| s.split_once(' '))
46 {
47 let authz_type = authz_type.to_lowercase();
48
49 if authz_type == "basic" {
50 (Some(authz_data.to_string()), None)
51 } else if authz_type == "bearer" {
52 if let Ok(jwsc) = JwsCompact::from_str(authz_data) {
53 (None, Some(jwsc))
54 } else {
55 warn!("bearer jws invalid");
56 (None, None)
57 }
58 } else {
59 warn!("authorisation header invalid, ignoring");
60 (None, None)
61 }
62 } else {
63 (None, None)
64 }
65 } else {
66 let jar = CookieJar::from_headers(&parts.headers);
68
69 let value: Option<&str> = jar.get(COOKIE_BEARER_TOKEN).map(|c| c.value());
70
71 let maybe_bearer = value.and_then(|authz_data| JwsCompact::from_str(authz_data).ok());
72
73 (None, maybe_bearer)
74 };
75
76 let mut client_auth_info = ClientAuthInfo::new(
77 Source::Https(client_ip_addr),
78 client_cert,
79 bearer_token,
80 basic_authz,
81 );
82
83 let _ = state
87 .qe_r_ref
88 .pre_validate_client_auth_info(&mut client_auth_info)
89 .await;
90
91 Ok(VerifiedClientInformation(client_auth_info))
92 }
93}
94
95pub struct AuthorisationHeaders(pub ClientAuthInfo);
96
97#[async_trait]
98impl FromRequestParts<ServerState> for AuthorisationHeaders {
99 type Rejection = (StatusCode, &'static str);
100
101 #[instrument(level = "debug", skip_all)]
103 async fn from_request_parts(
104 parts: &mut Parts,
105 _state: &ServerState,
106 ) -> Result<Self, Self::Rejection> {
107 let ClientConnInfo {
108 connection_addr: _,
109 client_ip_addr,
110 client_cert,
111 } = parts.extensions.remove::<ClientConnInfo>().ok_or((
112 StatusCode::INTERNAL_SERVER_ERROR,
113 "request info contains invalid data",
114 ))?;
115
116 let (basic_authz, bearer_token) = if let Some(header) = parts.headers.get(AUTHORISATION) {
117 if let Some((authz_type, authz_data)) = header
118 .to_str()
119 .map_err(|err| {
120 warn!(?err, "Invalid authz header, ignoring");
121 })
122 .ok()
123 .and_then(|s| s.split_once(' '))
124 {
125 let authz_type = authz_type.to_lowercase();
126
127 if authz_type == "basic" {
128 (Some(authz_data.to_string()), None)
129 } else if authz_type == "bearer" {
130 if let Ok(jwsc) = JwsCompact::from_str(authz_data) {
131 (None, Some(jwsc))
132 } else {
133 warn!("bearer jws invalid");
134 (None, None)
135 }
136 } else {
137 warn!("authorisation header invalid, ignoring");
138 (None, None)
139 }
140 } else {
141 (None, None)
142 }
143 } else {
144 (None, None)
145 };
146
147 let client_auth_info = ClientAuthInfo::new(
148 Source::Https(client_ip_addr),
149 client_cert,
150 bearer_token,
151 basic_authz,
152 );
153
154 Ok(AuthorisationHeaders(client_auth_info))
155 }
156}
157
158pub struct DomainInfo(pub DomainInfoRead);
159
160#[async_trait]
161impl FromRequestParts<ServerState> for DomainInfo {
162 type Rejection = (StatusCode, &'static str);
163
164 #[instrument(level = "debug", skip_all)]
166 async fn from_request_parts(
167 _parts: &mut Parts,
168 state: &ServerState,
169 ) -> Result<Self, Self::Rejection> {
170 Ok(DomainInfo(state.qe_r_ref.domain_info_read()))
171 }
172}
173
174#[derive(Debug, Clone)]
175pub struct ClientConnInfo {
176 #[allow(dead_code)]
179 pub connection_addr: SocketAddr,
180 pub client_ip_addr: IpAddr,
183 pub client_cert: Option<ClientCertInfo>,
185}
186
187impl Connected<ClientConnInfo> for ClientConnInfo {
189 fn connect_info(target: ClientConnInfo) -> Self {
190 target
191 }
192}
193
194impl Connected<SocketAddr> for ClientConnInfo {
196 fn connect_info(connection_addr: SocketAddr) -> Self {
197 ClientConnInfo {
198 client_ip_addr: connection_addr.ip().to_canonical(),
199 connection_addr,
200 client_cert: None,
201 }
202 }
203}