This commit is contained in:
Mateusz Gruszczyński committed 2026-10-19 11:09:48 +02:00
1 parent 4afa88a65e
commit 2ab44c4c62
49 files changed
+1523 -176

No files matched your search

+39 -6
View File
@@ -120,8 +120,7 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
// Pinned browser origin is panel-only. Certificate automation using API
// credentials keeps the original forwarding/TLS authorization semantics.
let automation_credentials = headers.contains_key(CERTIFICATE_API_KEY_HEADER)
|| (headers.contains_key(AUTHORIZATION)
&& certificate_api_operation(request.method(), request.uri().path()).is_some());
|| headers.contains_key(AUTHORIZATION);
let pinned_authority_result = if automation_credentials {
Ok(None)
} else {
@@ -173,7 +172,10 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
let secret = auth::cookie_secret(headers);
let certificate_api_operation = certificate_api_operation(request.method(), &path);
let mut certificate_api_access = None;
let certificate_api_session = if let Some(operation) = certificate_api_operation.as_ref() {
let certificate_api_session = if headers.contains_key(CERTIFICATE_API_KEY_HEADER) {
let Some(operation) = certificate_api_operation.as_ref() else {
return secure(reject(StatusCode::FORBIDDEN, "certificate_api.invalid_route"));
};
match certificate_api_credentials(headers) {
Ok(None) => None,
Ok(Some((key_id, token))) => {
@@ -241,7 +243,36 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
} else {
None
};
let external_certificate_api = certificate_api_session.is_some();
let general_api_session = if certificate_api_session.is_none() && headers.contains_key(AUTHORIZATION) {
// No cookies are accepted as a fallback when a malformed Bearer header is supplied.
let bearer = single_header(headers, AUTHORIZATION.as_str())
.and_then(|header| header.strip_prefix("Bearer "));
let Some(token) = bearer.filter(|value| valid_certificate_api_token(value)) else {
return secure(reject(StatusCode::UNAUTHORIZED, "api_token.invalid_token"));
};
let Some(peer) = peer else {
return secure(reject(StatusCode::FORBIDDEN, "api_token.peer_required"));
};
if !peer.0.ip().is_loopback() && scheme != "https" {
return secure(reject(StatusCode::FORBIDDEN, "api_token.https_required"));
}
let hash = auth::secret_hash(token);
let peer_ip = crate::vhost::canonical_ip(peer.0.ip());
let grant = match state.store.verify_api_token(&hash, peer_ip).await {
Ok(Some(grant)) => grant,
Ok(None) => return secure(reject(StatusCode::UNAUTHORIZED, "api_token.invalid_token_or_ip")),
Err(error) => return secure(auth::internal(error).into_response()),
};
if !general_api_token_allowed(request.method(), &path, &grant.permission) {
return secure(reject(StatusCode::FORBIDDEN, "api_token.insufficient_permission"));
}
Some(Session {
token_hash: hash, csrf: String::new(),
username: format!("api-token:{}", grant.id),
expires_at: auth::now().saturating_add(60), generation: 0,
})
} else { None };
let external_credential = certificate_api_session.is_some() || general_api_session.is_some();
let session_seconds = state
.config
.load()
@@ -254,6 +285,8 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
// login resources and capability discovery never load operational state.
let session = if certificate_api_session.is_some() {
certificate_api_session
} else if general_api_session.is_some() {
general_api_session
} else if !public || panel_document_path(&path) {
match secret.as_deref() {
Some(secret) => match state
@@ -272,7 +305,7 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
if !public && session.is_none() {
return secure(unauthorized(&request));
}
if unsafe_method && !external_certificate_api {
if unsafe_method && !external_credential {
if single_header(headers, "origin")
.is_none_or(|origin| !admin_origin_matches(origin, scheme, &external_authority))
|| single_header(headers, "x-rustproxy-request") != Some("1")
@@ -304,7 +337,7 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
path.as_str(),
"/api/auth/logout" | "/api/auth/logout-all" | "/api/auth/password"
);
if refresh_session_cookie {
if refresh_session_cookie && !external_credential {
if let (Some(secret), Some(session)) = (secret.as_deref(), session.as_ref()) {
let remaining = session.expires_at.saturating_sub(auth::now());
if remaining > 0 {