1use core::fmt;
6
7use mkit_core::hash::hash;
8use subtle::ConstantTimeEq;
9
10use crate::auth_v2::{self, AuthV2Config};
11use crate::error::{Redacted, ServerError};
12use crate::op::{Procedure, VerifiedAuth};
13use crate::principal::Principal;
14use crate::repo::ResolvedRepo;
15
16#[derive(Debug, Clone)]
18#[non_exhaustive]
19pub enum AuthMode {
20 Open,
23 Bearer {
29 token: Redacted,
31 },
32 AuthV2(AuthV2Config),
36 TransportIdentity,
39}
40
41pub type HeaderValues<'a> = dyn Fn(&str) -> Vec<String> + 'a;
43
44pub struct RequestMeta<'a> {
46 pub procedure: Procedure,
48 pub header: &'a dyn Fn(&str) -> Option<String>,
50 pub header_values: Option<&'a HeaderValues<'a>>,
52 pub unary_body: Option<&'a [u8]>,
56 pub transport_principal: Option<Principal>,
58}
59
60impl fmt::Debug for RequestMeta<'_> {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 f.debug_struct("RequestMeta")
64 .field("procedure", &self.procedure)
65 .field("unary_body", &self.unary_body.map(<[u8]>::len))
66 .field("transport_principal", &self.transport_principal)
67 .finish_non_exhaustive()
68 }
69}
70
71#[derive(Clone, PartialEq, Eq)]
74pub struct Authenticated {
75 pub principal: Principal,
77 pub auth: Option<VerifiedAuth>,
79 pub write_grant: Option<Redacted>,
81 pub(super) credential_capture: Vec<super::admission::CapturedCredential>,
84 pub ref_hint: Option<String>,
86 procedure: Procedure,
87 repo: ResolvedRepo,
88 pub(crate) business_skew_ms: i64,
91 pub(crate) business_now_ms: i64,
93 #[cfg(feature = "test-faults")]
94 directives: super::TestDirectives,
95}
96
97impl fmt::Debug for Authenticated {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 f.debug_struct("Authenticated")
100 .field("principal", &self.principal)
101 .field("auth", &self.auth)
102 .field("procedure", &self.procedure)
103 .field("repo", &self.repo)
104 .finish_non_exhaustive()
105 }
106}
107
108impl Authenticated {
109 #[must_use]
112 pub fn procedure(&self) -> Procedure {
113 self.procedure
114 }
115
116 #[must_use]
118 pub fn repo(&self) -> &ResolvedRepo {
119 &self.repo
120 }
121
122 #[cfg(feature = "test-faults")]
124 #[must_use]
125 pub fn test_directives(&self) -> &super::TestDirectives {
126 &self.directives
127 }
128
129 #[cfg(feature = "test-faults")]
130 pub(crate) fn set_test_directives(&mut self, directives: super::TestDirectives) {
131 self.directives = directives;
132 }
133}
134
135pub(crate) fn signed_request(mode: &AuthMode, meta: &RequestMeta<'_>) -> bool {
139 matches!(mode, AuthMode::AuthV2(_))
140 && (meta.procedure.is_write() && meta.procedure != Procedure::SetRepoVisibility
141 || auth_v2::carries_auth_headers(meta.header))
142}
143
144pub(crate) fn authenticate(
146 mode: &AuthMode,
147 meta: &RequestMeta<'_>,
148 now_ms: i64,
149 repo: ResolvedRepo,
150 expected_repository: &str,
151) -> Result<Authenticated, ServerError> {
152 let procedure = meta.procedure;
153 let presented_grant = (meta.header)("x-write-grant").map(Redacted::new);
154 let (principal, auth) = match mode {
155 AuthMode::Bearer { token } => {
156 let got = (meta.header)("authorization").unwrap_or_default();
157 let expected = format!("Bearer {}", token.expose());
158 let same = hash(got.as_bytes()).ct_eq(&hash(expected.as_bytes()));
159 if !bool::from(same) {
160 return Err(ServerError::unauthenticated(
161 "missing or invalid Authorization: Bearer <token>",
162 ));
163 }
164 (Principal::BearerHolder, None)
165 }
166 AuthMode::AuthV2(cfg) => {
167 let must_sign = procedure.is_write() && procedure != Procedure::SetRepoVisibility;
168 if must_sign || auth_v2::carries_auth_headers(meta.header) {
169 let auth = verify_auth_v2(cfg, expected_repository, meta, now_ms)?;
173 (
174 Principal::Signer {
175 ed25519: auth.signer,
176 },
177 Some(auth),
178 )
179 } else if procedure == Procedure::IssueObjectUrl {
180 return Err(ServerError::unauthenticated(
181 "IssueObjectUrl requires auth v2 authorization",
182 ));
183 } else {
184 (Principal::Anonymous, None)
185 }
186 }
187 AuthMode::Open => (Principal::Anonymous, None),
188 AuthMode::TransportIdentity => (
189 meta.transport_principal
190 .clone()
191 .ok_or_else(|| ServerError::unauthenticated("missing transport identity"))?,
192 None,
193 ),
194 };
195 Ok(Authenticated {
199 principal,
200 auth,
201 write_grant: presented_grant,
202 credential_capture: Vec::new(),
203 procedure,
204 repo,
205 ref_hint: None,
206 business_skew_ms: 0,
207 business_now_ms: now_ms,
208 #[cfg(feature = "test-faults")]
209 directives: super::TestDirectives::default(),
210 })
211}
212
213fn verify_auth_v2(
214 cfg: &AuthV2Config,
215 repository: &str,
216 meta: &RequestMeta<'_>,
217 now_ms: i64,
218) -> Result<VerifiedAuth, ServerError> {
219 let headers = auth_v2::headers_from(meta.header);
220 let path = meta.procedure.connect_path();
221 if meta.procedure.is_streaming() && meta.procedure != Procedure::DownloadPack {
222 return auth_v2::verify_stream_for(cfg, repository, path, now_ms, &headers);
223 }
224 let body = meta.unary_body.ok_or_else(|| {
225 ServerError::internal("authentication failed", "unary request without its body")
226 })?;
227 auth_v2::verify_unary_for(cfg, repository, path, body, now_ms, &headers)
228}