chasing weirdness (#1910)

* security headers, fixing error on empty username, handling login without SPN better

* making deno happy

* cleaning up windows build
This commit is contained in:
James Hodgkinson 2023-07-31 10:49:59 +10:00 committed by GitHub
parent 43372b7194
commit ea4d755d7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 120 additions and 57 deletions

View file

@ -1,18 +1,13 @@
--- ---
name: Windows Build and Test name: Windows Build and Test
# at the moment this only builds the kanidm client
# because @yaleman got tired but it's enough to prove
# it builds and be able to administer Kanidm from
# Windows-land
# Trigger the workflow on push to master or pull request # Trigger the workflow on push to master or pull request
"on": "on":
push: push:
branches: branches:
- master - master
pull_request: pull_request:
workflow_dispatch: # so you can run it manually
env: env:
SCCACHE_GHA_ENABLED: "true" SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache" RUSTC_WRAPPER: "sccache"
@ -36,6 +31,6 @@ jobs:
uses: mozilla-actions/sccache-action@v0.0.3 uses: mozilla-actions/sccache-action@v0.0.3
with: with:
version: "v0.4.2" version: "v0.4.2"
- run: cargo build -p kanidm_client -p kanidm_tools -p orca -p daemon - run: cargo build -p kanidm_client -p kanidm_tools --bin kanidm
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
- run: cargo test -p kanidm_client -p kanidm_tools -p orca -p daemon -p kanidmd_core - run: cargo test -p kanidm_client -p kanidm_tools

View file

@ -107,16 +107,16 @@ This is how it works in the automated build:
1. Enable use of installed packages for the user system-wide: 1. Enable use of installed packages for the user system-wide:
```bash ```bash
vcpkg integrate install vcpkg integrate install
``` ```
2. Install the openssl dependency, which compiles it from source. This downloads all sorts of 2. Install the openssl dependency, which compiles it from source. This downloads all sorts of
dependencies, including perl for the build. dependencies, including perl for the build.
```bash ```bash
vcpkg install openssl:x64-windows-static-md vcpkg install openssl:x64-windows-static-md
``` ```
There's a powershell script in the root directory of the repository which, in concert with `openssl` There's a powershell script in the root directory of the repository which, in concert with `openssl`
will generate a config file and certs for testing. will generate a config file and certs for testing.

View file

@ -1,21 +0,0 @@
use axum::extract::State;
use axum::http::Request;
use axum::middleware::Next;
use axum::response::Response;
use crate::https::ServerState;
pub async fn cspheaders_layer<B>(
State(state): State<ServerState>,
request: Request<B>,
next: Next<B>,
) -> Response {
// wait for the middleware to come back
let mut response = next.run(request).await;
// add the header
let headers = response.headers_mut();
headers.insert("Content-Security-Policy", state.csp_header);
response
}

View file

@ -10,7 +10,7 @@ use uuid::Uuid;
pub(crate) mod caching; pub(crate) mod caching;
pub(crate) mod compression; pub(crate) mod compression;
pub(crate) mod csp_headers; pub(crate) mod security_headers;
pub(crate) mod hsts_header; pub(crate) mod hsts_header;
// the version middleware injects // the version middleware injects

View file

@ -0,0 +1,58 @@
use axum::extract::State;
use axum::http::Request;
use axum::middleware::Next;
use axum::response::Response;
use http::header::X_CONTENT_TYPE_OPTIONS;
use http::HeaderValue;
use crate::https::ServerState;
const PERMISSIONS_POLICY_VALUE: &str = "fullscreen=(), geolocation=()";
const X_CONTENT_TYPE_OPTIONS_VALUE: &str = "nosniff";
pub async fn security_headers_layer<B>(
State(state): State<ServerState>,
request: Request<B>,
next: Next<B>,
) -> Response {
// wait for the middleware to come back
let mut response = next.run(request).await;
// add the Content-Security-Policy header, which defines how contact will be accessed/run based on the source URL
let headers = response.headers_mut();
headers.insert(http::header::CONTENT_SECURITY_POLICY, state.csp_header);
// X-Content-Type-Options tells the browser if it's OK to "sniff" or guess the content type of a response
//
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
// https://scotthelme.co.uk/hardening-your-http-response-headers/#x-content-type-options
#[allow(clippy::expect_used)]
headers.insert(
X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_str(X_CONTENT_TYPE_OPTIONS_VALUE)
.expect("Failed to generate security header X-Content-Type-Options"),
);
// Permissions policy defines access to platform services like geolocation, fullscreen etc.
//
// https://www.w3.org/TR/permissions-policy-1/
#[allow(clippy::expect_used)]
headers.insert(
"Permissions-Policy",
HeaderValue::from_str(PERMISSIONS_POLICY_VALUE)
.expect("Failed to generate security header Permissions-Policy"),
);
// Don't send a referrer header when the user is navigating to a non-HTTPS URL
// Ref:
// https://scotthelme.co.uk/a-new-security-header-referrer-policy/
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
#[allow(clippy::expect_used)]
headers.insert(
http::header::REFERRER_POLICY,
HeaderValue::from_str("no-referrer-when-downgrade")
.expect("Failed to generate Referer-Policy header"),
);
response
}

View file

@ -234,7 +234,7 @@ pub async fn create_https_server(
.merge(static_routes) .merge(static_routes)
.layer(from_fn_with_state( .layer(from_fn_with_state(
state.clone(), state.clone(),
middleware::csp_headers::cspheaders_layer, middleware::security_headers::security_headers_layer,
)) ))
.layer(from_fn(middleware::version_middleware)) .layer(from_fn(middleware::version_middleware))
.layer(from_fn( .layer(from_fn(

View file

@ -1261,9 +1261,7 @@ pub async fn auth(
.qe_r_ref .qe_r_ref
.handle_auth(maybe_sessionid, obj, kopid.eventid, ip_addr) .handle_auth(maybe_sessionid, obj, kopid.eventid, ip_addr)
.await; .await;
debug!("Auth result: {:?}", inter); debug!("Auth result: {:?}", inter);
auth_session_state_management(state, inter) auth_session_state_management(state, inter)
} }

View file

@ -302,17 +302,27 @@ pub enum AuthEventStep {
impl AuthEventStep { impl AuthEventStep {
fn from_authstep(aus: AuthStep, sid: Option<Uuid>) -> Result<Self, OperationError> { fn from_authstep(aus: AuthStep, sid: Option<Uuid>) -> Result<Self, OperationError> {
match aus { match aus {
AuthStep::Init(username) => Ok(AuthEventStep::Init(AuthEventStepInit { AuthStep::Init(username) => {
username, if username.trim().is_empty() {
issue: AuthIssueSession::Token, Err(OperationError::EmptyRequest)
})), } else {
Ok(AuthEventStep::Init(AuthEventStepInit {
username,
issue: AuthIssueSession::Token,
}))
}
}
AuthStep::Init2 { username, issue } => { AuthStep::Init2 { username, issue } => {
Ok(AuthEventStep::Init(AuthEventStepInit { username, issue })) if username.trim().is_empty() {
Err(OperationError::EmptyRequest)
} else {
Ok(AuthEventStep::Init(AuthEventStepInit { username, issue }))
}
} }
AuthStep::Begin(mech) => match sid { AuthStep::Begin(mech) => match sid {
Some(ssid) => Ok(AuthEventStep::Begin(AuthEventStepMech { Some(sessionid) => Ok(AuthEventStep::Begin(AuthEventStepMech {
sessionid: ssid, sessionid,
mech, mech,
})), })),
None => Err(OperationError::InvalidAuthState( None => Err(OperationError::InvalidAuthState(
@ -320,8 +330,8 @@ impl AuthEventStep {
)), )),
}, },
AuthStep::Cred(cred) => match sid { AuthStep::Cred(cred) => match sid {
Some(ssid) => Ok(AuthEventStep::Cred(AuthEventStepCred { Some(sessionid) => Ok(AuthEventStep::Cred(AuthEventStepCred {
sessionid: ssid, sessionid,
cred, cred,
})), })),
None => Err(OperationError::InvalidAuthState( None => Err(OperationError::InvalidAuthState(

View file

@ -110,20 +110,43 @@ impl CommonOpt {
.get(filter_username) .get(filter_username)
.map(|t| (filter_username.clone(), t.clone())) .map(|t| (filter_username.clone(), t.clone()))
} else { } else {
let filter_username = format!("{}@", filter_username); // first we try to find user@hostname
// First, filter for tokens that match. let filter_username_with_hostname = format!(
"{}@{}",
filter_username,
client.get_origin().host_str().unwrap_or("localhost")
);
debug!(
"Looking for tokens matching {}",
filter_username_with_hostname
);
let mut token_refs: Vec<_> = tokens let mut token_refs: Vec<_> = tokens
.iter() .iter()
.filter(|(t, _)| t.starts_with(&filter_username)) .filter(|(t, _)| *t == &filter_username_with_hostname)
.map(|(k, v)| (k.clone(), v.clone())) .map(|(k, v)| (k.clone(), v.clone()))
.collect(); .collect();
match token_refs.len() { if token_refs.len() == 1 {
0 => None, // return the token
1 => token_refs.pop(), token_refs.pop()
_ => { } else {
error!("Multiple authentication tokens found for {}. Please specify the full spn to proceed", filter_username); // otherwise let's try the fallback
return Err(ToClientError::Other); let filter_username = format!("{}@", filter_username);
// Filter for tokens that match the pattern
let mut token_refs: Vec<_> = tokens
.into_iter()
.filter(|(t, _)| t.starts_with(&filter_username))
.map(|(k, v)| (k, v))
.collect();
match token_refs.len() {
0 => None,
1 => token_refs.pop(),
_ => {
error!("Multiple authentication tokens found for {}. Please specify the full spn to proceed", filter_username);
return Err(ToClientError::Other);
}
} }
} }
}; };