From d1f51f0a84bf4ae1fd31f12e90bab09c6eeaea97 Mon Sep 17 00:00:00 2001 From: Firstyear Date: Thu, 6 Jul 2023 19:34:53 +1000 Subject: [PATCH] 1812 1813 post axum cleanup (#1817) --- server/core/src/https/extractors/mod.rs | 78 ++++++++ server/core/src/https/middleware/mod.rs | 27 +-- server/core/src/https/mod.rs | 37 ++-- server/core/src/https/oauth2.rs | 41 ++-- server/core/src/https/v1.rs | 253 ++++++++++++------------ server/core/src/https/v1_scim.rs | 4 +- server/core/src/lib.rs | 2 +- server/daemon/src/main.rs | 22 +-- server/lib/src/constants/entries.rs | 2 +- server/lib/src/idm/server.rs | 4 +- server/lib/src/plugins/domain.rs | 2 +- server/lib/src/server/migrations.rs | 17 ++ server/lib/src/server/mod.rs | 4 +- 13 files changed, 280 insertions(+), 213 deletions(-) create mode 100644 server/core/src/https/extractors/mod.rs diff --git a/server/core/src/https/extractors/mod.rs b/server/core/src/https/extractors/mod.rs new file mode 100644 index 000000000..0770b1517 --- /dev/null +++ b/server/core/src/https/extractors/mod.rs @@ -0,0 +1,78 @@ +use axum::{ + async_trait, + extract::{ConnectInfo, FromRequestParts}, + http::{header::HeaderName, request::Parts, StatusCode}, + RequestPartsExt, +}; + +use std::net::{IpAddr, SocketAddr}; + +use crate::https::ServerState; + +#[allow(clippy::declare_interior_mutable_const)] +const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for"); + +pub struct TrustedClientIp(pub IpAddr); + +#[async_trait] +impl FromRequestParts for TrustedClientIp { + type Rejection = (StatusCode, &'static str); + + #[instrument(level = "debug", skip(state))] + async fn from_request_parts( + parts: &mut Parts, + state: &ServerState, + ) -> Result { + if state.trust_x_forward_for { + if let Some(x_forward_for) = parts.headers.get(X_FORWARDED_FOR) { + // X forward for may be comma separate. + let first = x_forward_for + .to_str() + .map(|s| + // Split on an optional comma, return the first result. + s.split(',').next().unwrap_or(s)) + .map_err(|_| { + ( + StatusCode::BAD_REQUEST, + "X-Forwarded-For contains invalid data", + ) + })?; + + first.parse::().map(TrustedClientIp).map_err(|_| { + ( + StatusCode::BAD_REQUEST, + "X-Forwarded-For contains invalid ip addr", + ) + }) + } else { + let ConnectInfo(addr) = + parts + .extract::>() + .await + .map_err(|_| { + error!("Connect info contains invalid IP address"); + ( + StatusCode::BAD_REQUEST, + "connect info contains invalid IP address", + ) + })?; + + Ok(TrustedClientIp(addr.ip())) + } + } else { + let ConnectInfo(addr) = + parts + .extract::>() + .await + .map_err(|_| { + error!("Connect info contains invalid IP address"); + ( + StatusCode::BAD_REQUEST, + "connect info contains invalid IP address", + ) + })?; + + Ok(TrustedClientIp(addr.ip())) + } + } +} diff --git a/server/core/src/https/middleware/mod.rs b/server/core/src/https/middleware/mod.rs index 04333afb8..7313fcf7b 100644 --- a/server/core/src/https/middleware/mod.rs +++ b/server/core/src/https/middleware/mod.rs @@ -3,7 +3,7 @@ use axum::{ http::{self, Request}, middleware::Next, response::Response, - Extension, TypedHeader, + TypedHeader, }; use axum_sessions::SessionHandle; use http::HeaderValue; @@ -21,7 +21,6 @@ pub async fn version_middleware(request: Request, next: Next) -> Respon let mut response = next.run(request).await; let headers = response.headers_mut(); headers.insert("X-KANIDM-VERSION", HeaderValue::from_static(KANIDM_VERSION)); - response } @@ -32,16 +31,9 @@ pub struct KOpId { pub uat: Option, } -impl KOpId { - /// Return the event ID as a string - pub fn eventid_value(&self) -> String { - let res = self.eventid; - res.as_hyphenated().to_string() - } -} - /// This runs at the start of the request, adding an extension with `KOpId` which has useful things inside it. -pub async fn kopid_start( +#[instrument(name = "request", skip_all)] +pub async fn kopid_middleware( auth: Option>>, mut request: Request, next: Next, @@ -66,23 +58,12 @@ pub async fn kopid_start( // insert the extension so we can pull it out later request.extensions_mut().insert(KOpId { eventid, uat }); - next.run(request).await -} - -/// This runs at the start of the request, adding an extension with the OperationID -pub async fn kopid_end( - Extension(kopid): Extension, - request: Request, - next: Next, -) -> Response { - // generate the event ID - // insert the extension so we can pull it out later let mut response = next.run(request).await; #[allow(clippy::unwrap_used)] response.headers_mut().insert( "X-KANIDM-OPID", - HeaderValue::from_str(&kopid.eventid_value()).unwrap(), + HeaderValue::from_str(&eventid.as_hyphenated().to_string()).unwrap(), ); response diff --git a/server/core/src/https/mod.rs b/server/core/src/https/mod.rs index 3ff9c713b..28d13f86e 100644 --- a/server/core/src/https/mod.rs +++ b/server/core/src/https/mod.rs @@ -1,3 +1,4 @@ +mod extractors; mod generic; mod javascript; mod manifest; @@ -60,7 +61,7 @@ pub struct ServerState { pub jws_validator: compact_jwt::JwsValidator, // The SHA384 hashes of javascript files we're going to serve to users pub js_files: Vec, - // pub(crate) trust_x_forward_for: bool, + pub(crate) trust_x_forward_for: bool, pub csp_header: HeaderValue, } @@ -133,8 +134,7 @@ pub fn get_js_files(role: ServerRole) -> Vec { pub async fn create_https_server( config: Configuration, - // trust_x_forward_for: bool, // TODO: #1787 make XFF headers work - cookie_key: [u8; 32], + cookie_key: [u8; 64], jws_signer: JwsSigner, status_ref: &'static StatusActor, qe_w_ref: &'static QueryServerWriteV1, @@ -186,15 +186,16 @@ pub async fn create_https_server( ); let store = async_session::CookieStore::new(); - let secret = format!("{:?}", cookie_key); - let secret = secret.as_bytes(); // TODO the cookie/session secret needs to be longer? - let session_layer = SessionLayer::new(store, secret) + + let session_layer = SessionLayer::new(store, &cookie_key) .with_cookie_name("kanidm-session") .with_session_ttl(None) .with_cookie_domain(config.domain) .with_same_site_policy(SameSite::Strict) .with_secure(true); + let trust_x_forward_for = config.trust_x_forward_for; + let state = ServerState { status_ref, qe_w_ref, @@ -202,6 +203,7 @@ pub async fn create_https_server( jws_signer, jws_validator, js_files, + trust_x_forward_for, csp_header: csp_header.finish(), }; @@ -219,15 +221,13 @@ pub async fn create_https_server( ServerRole::WriteReplicaNoUI => Router::new(), }; - // // == oauth endpoints. - // TODO: turn this from a nest into a merge because state things are bad in nested routes let app = Router::new() - .nest("/oauth2", oauth2::oauth2_route_setup(state.clone())) - .nest("/scim", v1_scim::scim_route_setup()) .route("/robots.txt", get(robots_txt)) - .nest("/v1", v1::router(state.clone())) - // Shared account features only - mainly this is for unix-like features. - .route("/status", get(status)); + .route("/status", get(status)) + .merge(oauth2::oauth2_route_setup(state.clone())) + .merge(v1_scim::scim_route_setup()) + .merge(v1::router(state.clone())); + let app = match config.role { ServerRole::WriteReplicaNoUI => app, ServerRole::WriteReplica | ServerRole::ReadOnlyReplica => { @@ -250,11 +250,14 @@ pub async fn create_https_server( middleware::csp_headers::cspheaders_layer, )) .layer(from_fn(middleware::version_middleware)) - .layer(from_fn(middleware::kopid_end)) - .layer(from_fn(middleware::kopid_start)) .layer(session_layer) - .with_state(state) .layer(TraceLayer::new_for_http()) + // This must be the LAST middleware. + // This is because the last middleware here is the first to be entered and the last + // to be exited, and this middleware sets up ids' and other bits for for logging + // coherence to be maintained. + .layer(from_fn(middleware::kopid_middleware)) + .with_state(state) // the connect_info bit here lets us pick up the remote address of the client .into_make_service_with_connect_info::(); @@ -301,7 +304,7 @@ async fn server_loop( let mut tls_builder = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls())?; let mut app = app; tls_builder - .set_certificate_file(tls_param.chain.clone(), SslFiletype::PEM) + .set_certificate_chain_file(tls_param.chain.clone()) .map_err(|err| { std::io::Error::new( ErrorKind::Other, diff --git a/server/core/src/https/oauth2.rs b/server/core/src/https/oauth2.rs index 43b9bafa1..15453bce3 100644 --- a/server/core/src/https/oauth2.rs +++ b/server/core/src/https/oauth2.rs @@ -59,7 +59,6 @@ pub async fn oauth2_id_get( Path(rs_name): Path, Extension(kopid): Extension, ) -> impl IntoResponse { - let filter = oauth2_id(&rs_name); let res = state @@ -227,8 +226,6 @@ pub async fn oauth2_id_delete( // valid Kanidm instance in the topology can handle these request. // - - pub async fn oauth2_authorise_post( State(state): State, Extension(kopid): Extension, @@ -349,8 +346,8 @@ async fn oauth2_authorise( // we do NOT redirect in an error condition, and just render the error ourselves. Err(e) => { admin_error!( - "Unable to authorise - Error ID: {} error: {}", - &kopid.eventid_value(), + "Unable to authorise - Error ID: {:?} error: {}", + kopid.eventid, &e.to_string() ); #[allow(clippy::unwrap_used)] @@ -514,7 +511,7 @@ async fn oauth2_authorise_reject( pub async fn oauth2_token_post( State(state): State, Extension(kopid): Extension, - headers: HeaderMap, // TOOD: make this a typed basic auth header + headers: HeaderMap, // TODO: make this a typed basic auth header Form(tok_req): Form, ) -> impl IntoResponse { // This is called directly by the resource server, where we then issue @@ -834,52 +831,58 @@ pub async fn oauth2_token_revoke_post( pub fn oauth2_route_setup(state: ServerState) -> Router { // this has all the openid-related routes - let openid_router = Router::new() // appserver.at("/oauth2/openid"); + let openid_router = Router::new() // // ⚠️ ⚠️ WARNING ⚠️ ⚠️ // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS .route( - "/:client_id/.well-known/openid-configuration", + "/oauth2/openid/:client_id/.well-known/openid-configuration", get(oauth2_openid_discovery_get), ) // // ⚠️ ⚠️ WARNING ⚠️ ⚠️ // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - .route("/:client_id/userinfo", get(oauth2_openid_userinfo_get)) + .route( + "/oauth2/openid/:client_id/userinfo", + get(oauth2_openid_userinfo_get), + ) // // ⚠️ ⚠️ WARNING ⚠️ ⚠️ // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS .route( - "/:client_id/public_key.jwk", + "/oauth2/openid/:client_id/public_key.jwk", get(oauth2_openid_publickey_get), ) .with_state(state.clone()); - Router::new() //= appserver.at("/oauth2"); - .route("/", get(oauth2_get)) + Router::new() + .route("/oauth2", get(oauth2_get)) // ⚠️ ⚠️ WARNING ⚠️ ⚠️ // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS .route( - "/authorise", + "/oauth2/authorise", post(oauth2_authorise_post).get(oauth2_authorise_get), ) // ⚠️ ⚠️ WARNING ⚠️ ⚠️ // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS .route( - "/authorise/permit", + "/oauth2/authorise/permit", post(oauth2_authorise_permit_post).get(oauth2_authorise_permit_get), ) // ⚠️ ⚠️ WARNING ⚠️ ⚠️ // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS .route( - "/authorise/reject", + "/oauth2/authorise/reject", post(oauth2_authorise_reject_post).get(oauth2_authorise_reject_get), ) // ⚠️ ⚠️ WARNING ⚠️ ⚠️ // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - .route("/token", post(oauth2_token_post)) + .route("/oauth2/token", post(oauth2_token_post)) // ⚠️ ⚠️ WARNING ⚠️ ⚠️ // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - .route("/token/introspect", post(oauth2_token_introspect_post)) - .route("/token/revoke", post(oauth2_token_revoke_post)) - .nest("/openid", openid_router) + .route( + "/oauth2/token/introspect", + post(oauth2_token_introspect_post), + ) + .route("/oauth2/token/revoke", post(oauth2_token_revoke_post)) + .merge(openid_router) .with_state(state) .layer(from_fn(super::middleware::caching::dont_cache_me)) } diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs index 20847da29..095c07a8d 100644 --- a/server/core/src/https/v1.rs +++ b/server/core/src/https/v1.rs @@ -1,9 +1,6 @@ -use std::net::SocketAddr; -#[allow(unused_imports)] -// //! The V1 API things! -use std::str::FromStr; +//! The V1 API things! -use axum::extract::{ConnectInfo, Path, Query, State}; +use axum::extract::{Path, Query, State}; use axum::headers::{CacheControl, HeaderMapExt}; use axum::middleware::from_fn; use axum::response::{IntoResponse, Response}; @@ -29,6 +26,7 @@ use kanidmd_lib::value::PartialValue; use serde::{Deserialize, Serialize}; use uuid::Uuid; +use crate::https::extractors::TrustedClientIp; use crate::https::to_axum_response; use super::middleware::caching::dont_cache_me; @@ -1244,24 +1242,15 @@ pub async fn applinks_get( pub async fn reauth( State(state): State, - ConnectInfo(addr): ConnectInfo, // TODO: test x-ff-headers + TrustedClientIp(ip_addr): TrustedClientIp, Extension(kopid): Extension, session: WritableSession, Json(obj): Json, ) -> impl IntoResponse { - // TODO: xff things check that we can get the remote IP address first, since this doesn't touch the backend at all - // let ip_addr = req.get_remote_addr().ok_or_else(|| { - // error!("Unable to get remote addr for auth event, refusing to proceed"); - // tide::Error::from_str( - // tide::StatusCode::InternalServerError, - // "unable to validate peer address", - // ) - // })?; - // This may change in the future ... let inter = state .qe_r_ref - .handle_reauth(kopid.uat, obj, kopid.eventid, addr.ip()) + .handle_reauth(kopid.uat, obj, kopid.eventid, ip_addr) .await; debug!("REAuth result: {:?}", inter); auth_session_state_management(state, inter, session) @@ -1269,28 +1258,16 @@ pub async fn reauth( pub async fn auth( State(state): State, + TrustedClientIp(ip_addr): TrustedClientIp, session: WritableSession, headers: HeaderMap, Extension(kopid): Extension, - ConnectInfo(addr): ConnectInfo, Json(obj): Json, ) -> impl IntoResponse { - // TODO: check this trusts the x-ff-header - let ip_addr = addr.ip(); - // check that we can get the remote IP address first, since this doesn't touch the backend at all - // let ip_addr = req.get_remote_addr().ok_or_else(|| { - // error!("Unable to get remote addr for auth event, refusing to proceed"); - // tide::Error::from_str( - // tide::StatusCode::InternalServerError, - // "unable to validate peer address", - // ) - // })?; - // First, deal with some state management. // Do anything here first that's needed like getting the session details // out of the req cookie. - // TODO let maybe_sessionid = state.get_current_auth_session_id(&headers, &session); debug!("Session ID: {:?}", maybe_sessionid); // We probably need to know if we allocate the cookie, that this is a @@ -1434,281 +1411,295 @@ pub async fn auth_valid( #[instrument(skip(state))] pub fn router(state: ServerState) -> Router { Router::new() - .route("/oauth2", get(super::oauth2::oauth2_get)) + .route("/v1/oauth2", get(super::oauth2::oauth2_get)) .route( - "/oauth2/:rs_name", + "/v1/oauth2/:rs_name", get(super::oauth2::oauth2_id_get) .patch(super::oauth2::oauth2_id_patch) .delete(super::oauth2::oauth2_id_delete), ) .route( - "/oauth2/:rs_name/_basic_secret", + "/v1/oauth2/:rs_name/_basic_secret", get(super::oauth2::oauth2_id_get_basic_secret), ) - .route("/oauth2/_basic", post(super::oauth2::oauth2_basic_post)) + .route("/v1/oauth2/_basic", post(super::oauth2::oauth2_basic_post)) .route( - "/oauth2/:rs_name/_scopemap/:group", + "/v1/oauth2/:rs_name/_scopemap/:group", post(super::oauth2::oauth2_id_scopemap_post) .delete(super::oauth2::oauth2_id_scopemap_delete), ) .route( - "/oauth2/:rs_name/_sup_scopemap/:group", + "/v1/oauth2/:rs_name/_sup_scopemap/:group", post(super::oauth2::oauth2_id_sup_scopemap_post) .delete(super::oauth2::oauth2_id_sup_scopemap_delete), ) - .route("/raw/create", post(create)) - .route("/raw/modify", post(v1_modify)) - .route("/raw/delete", post(v1_delete)) - .route("/raw/search", post(search)) - .route("/schema", get(schema_get)) + .route("/v1/raw/create", post(create)) + .route("/v1/raw/modify", post(v1_modify)) + .route("/v1/raw/delete", post(v1_delete)) + .route("/v1/raw/search", post(search)) + .route("/v1/schema", get(schema_get)) .route( - "/schema/attributetype", + "/v1/schema/attributetype", get(schema_attributetype_get), // post(|| async { "TODO" }) ) .route( - "/schema/attributetype/:id", + "/v1/schema/attributetype/:id", get(schema_attributetype_get_id), ) // .route("/schema/attributetype/:id", put(|| async { "TODO" }).patch(|| async { "TODO" })) .route( - "/schema/classtype", + "/v1/schema/classtype", get(schema_classtype_get), // .post(|| async { "TODO" }) ) .route( - "/schema/classtype/:id", + "/v1/schema/classtype/:id", get(schema_classtype_get_id) .put(|| async { "TODO" }) .patch(|| async { "TODO" }), ) - .route("/self", get(whoami)) - .route("/self/_uat", get(whoami_uat)) - .route("/self/_attr/:attr", get(|| async { "TODO" })) - .route("/self/_credential", get(|| async { "TODO" })) - .route("/self/_credential/:cid/_lock", get(|| async { "TODO" })) + .route("/v1/self", get(whoami)) + .route("/v1/self/_uat", get(whoami_uat)) + .route("/v1/self/_attr/:attr", get(|| async { "TODO" })) + .route("/v1/self/_credential", get(|| async { "TODO" })) + .route("/v1/self/_credential/:cid/_lock", get(|| async { "TODO" })) .route( - "/self/_radius", + "/v1/self/_radius", get(|| async { "TODO" }) .delete(|| async { "TODO" }) .post(|| async { "TODO" }), ) - .route("/self/_radius/_config", post(|| async { "TODO" })) - .route("/self/_radius/_config/:token", get(|| async { "TODO" })) + .route("/v1/self/_radius/_config", post(|| async { "TODO" })) + .route("/v1/self/_radius/_config/:token", get(|| async { "TODO" })) .route( - "/self/_radius/_config/:token/apple", + "/v1/self/_radius/_config/:token/apple", get(|| async { "TODO" }), ) // Applinks are the list of apps this account can access. - .route("/self/_applinks", get(applinks_get)) + .route("/v1/self/_applinks", get(applinks_get)) // Person routes - .route("/person", get(person_get)) - .route("/person", post(person_post)) + .route("/v1/person", get(person_get)) + .route("/v1/person", post(person_post)) .route( - "/person/:id", + "/v1/person/:id", get(person_id_get) .patch(account_id_patch) .delete(person_account_id_delete), ) .route( - "/person/:id/_attr/:attr", + "/v1/person/:id/_attr/:attr", get(account_id_get_attr) .put(account_id_put_attr) .post(account_id_post_attr) .delete(account_id_delete_attr), ) - .route("/person/:id/_lock", get(|| async { "TODO" })) - .route("/person/:id/_credential", get(|| async { "TODO" })) + .route("/v1/person/:id/_lock", get(|| async { "TODO" })) + .route("/v1/person/:id/_credential", get(|| async { "TODO" })) .route( - "/person/:id/_credential/_status", + "/v1/person/:id/_credential/_status", get(account_get_id_credential_status), ) .route( - "/person/:id/_credential/:cid/_lock", + "/v1/person/:id/_credential/:cid/_lock", get(|| async { "TODO" }), ) .route( - "/person/:id/_credential/_update", + "/v1/person/:id/_credential/_update", get(account_get_id_credential_update), ) .route( - "/person/:id/_credential/_update_intent/:ttl", + "/v1/person/:id/_credential/_update_intent/:ttl", get(account_get_id_credential_update_intent_ttl), ) .route( - "/person/:id/_credential/_update_intent", + "/v1/person/:id/_credential/_update_intent", get(account_get_id_credential_update_intent), ) .route( - "/person/:id/_ssh_pubkeys", + "/v1/person/:id/_ssh_pubkeys", get(account_get_id_ssh_pubkeys).post(account_post_id_ssh_pubkey), ) .route( - "/person/:id/_ssh_pubkeys/:tag", + "/v1/person/:id/_ssh_pubkeys/:tag", get(account_get_id_ssh_pubkey_tag).delete(account_delete_id_ssh_pubkey_tag), ) .route( - "/person/:id/_radius", + "/v1/person/:id/_radius", get(account_get_id_radius) .post(account_post_id_radius_regenerate) .delete(account_delete_id_radius), ) .route( - "/person/:id/_radius/_token", + "/v1/person/:id/_radius/_token", get(account_get_id_radius_token), ) // TODO: make this cacheable - .route("/person/:id/_unix", post(account_post_id_unix)) + .route("/v1/person/:id/_unix", post(account_post_id_unix)) .route( - "/person/:id/_unix/_credential", + "/v1/person/:id/_unix/_credential", put(account_put_id_unix_credential).delete(account_delete_id_unix_credential), ) // Service accounts .route( - "/service_account", + "/v1/service_account", get(service_account_get).post(service_account_post), ) .route( - "/service_account/", + "/v1/service_account/", get(service_account_get).post(service_account_post), ) .route( - "/service_account/:id", + "/v1/service_account/:id", get(service_account_id_get).delete(service_account_id_delete), ) .route( - "/service_account/:id/_attr/:attr", + "/v1/service_account/:id/_attr/:attr", get(account_id_get_attr) .put(account_id_put_attr) .post(account_id_post_attr) .delete(account_id_delete_attr), ) - .route("/service_account/:id/_lock", get(|| async { "TODO" })) + .route("/v1/service_account/:id/_lock", get(|| async { "TODO" })) .route( - "/service_account/:id/_into_person", + "/v1/service_account/:id/_into_person", post(service_account_into_person), ) .route( - "/service_account/:id/_api_token", + "/v1/service_account/:id/_api_token", post(service_account_api_token_post).get(service_account_api_token_get), ) .route( - "/service_account/:id/_api_token/:token_id", + "/v1/service_account/:id/_api_token/:token_id", delete(service_account_api_token_delete), ) - .route("/service_account/:id/_credential", get(|| async { "TODO" })) .route( - "/service_account/:id/_credential/_generate", - get(service_account_credential_generate), - ) - .route( - "/service_account/:id/_credential/_status", - get(account_get_id_credential_status), - ) - .route( - "/service_account/:id/_credential/:cid/_lock", + "/v1/service_account/:id/_credential", get(|| async { "TODO" }), ) .route( - "/service_account/:id/_ssh_pubkeys", + "/v1/service_account/:id/_credential/_generate", + get(service_account_credential_generate), + ) + .route( + "/v1/service_account/:id/_credential/_status", + get(account_get_id_credential_status), + ) + .route( + "/v1/service_account/:id/_credential/:cid/_lock", + get(|| async { "TODO" }), + ) + .route( + "/v1/service_account/:id/_ssh_pubkeys", get(account_get_id_ssh_pubkeys).post(account_post_id_ssh_pubkey), ) .route( - "/service_account/:id/_ssh_pubkeys/:tag", + "/v1/service_account/:id/_ssh_pubkeys/:tag", get(account_get_id_ssh_pubkey_tag).delete(account_delete_id_ssh_pubkey_tag), ) - .route("/service_account/:id/_unix", post(account_post_id_unix)) - .route("/account/:id/_unix/_auth", post(account_post_id_unix_auth)) + .route("/v1/service_account/:id/_unix", post(account_post_id_unix)) .route( - "/account/:id/_unix/_token", + "/v1/account/:id/_unix/_auth", + post(account_post_id_unix_auth), + ) + .route( + "/v1/account/:id/_unix/_token", post(account_get_id_unix_token).get(account_get_id_unix_token), // TODO: make this cacheable ) .route( - "/account/:id/_radius/_token", + "/v1/account/:id/_radius/_token", post(account_get_id_radius_token).get(account_get_id_radius_token), // TODO: make this cacheable ) - .route("/account/:id/_ssh_pubkeys", get(account_get_id_ssh_pubkeys)) .route( - "/account/:id/_ssh_pubkeys/:tag", + "/v1/account/:id/_ssh_pubkeys", + get(account_get_id_ssh_pubkeys), + ) + .route( + "/v1/account/:id/_ssh_pubkeys/:tag", get(account_get_id_ssh_pubkey_tag), ) .route( - "/account/:id/_user_auth_token", + "/v1/account/:id/_user_auth_token", get(account_get_id_user_auth_token), ) .route( - "/account/:id/_user_auth_token/:token_id", + "/v1/account/:id/_user_auth_token/:token_id", delete(account_user_auth_token_delete), ) .route( - "/credential/_exchange_intent", + "/v1/credential/_exchange_intent", post(credential_update_exchange_intent), ) - .route("/credential/_status", post(credential_update_status)) - .route("/credential/_update", post(credential_update_update)) - .route("/credential/_commit", post(credential_update_commit)) - .route("/credential/_cancel", post(credential_update_cancel)) + .route("/v1/credential/_status", post(credential_update_status)) + .route("/v1/credential/_update", post(credential_update_update)) + .route("/v1/credential/_commit", post(credential_update_commit)) + .route("/v1/credential/_cancel", post(credential_update_cancel)) // domain-things - .route("/domain", get(domain_get)) + .route("/v1/domain", get(domain_get)) .route( - "/domain/_attr/:attr", + "/v1/domain/_attr/:attr", get(domain_get_attr) .put(domain_put_attr) .delete(domain_delete_attr), ) - .route("/group/:id/_unix/_token", get(group_get_id_unix_token)) - .route("/group/:id/_unix", post(group_post_id_unix)) - .route("/group", get(group_get).post(group_post)) - .route("/group/:id", get(group_id_get).delete(group_id_delete)) + .route("/v1/group/:id/_unix/_token", get(group_get_id_unix_token)) + .route("/v1/group/:id/_unix", post(group_post_id_unix)) + .route("/v1/group", get(group_get).post(group_post)) + .route("/v1/group/:id", get(group_id_get).delete(group_id_delete)) .route( - "/group/:id/_attr/:attr", + "/v1/group/:id/_attr/:attr", delete(group_id_delete_attr) .get(group_id_get_attr) .put(group_id_put_attr) .post(group_id_post_attr), ) .with_state(state.clone()) - .route("/system", get(system_get)) + .route("/v1/system", get(system_get)) .route( - "/system/_attr/:attr", + "/v1/system/_attr/:attr", get(system_get_attr) .post(system_post_attr) .delete(system_delete_attr), ) - .route("/recycle_bin", get(recycle_bin_get)) - .route("/recycle_bin/:id", get(recycle_bin_id_get)) - .route("/recycle_bin/:id/_revive", post(recycle_bin_revive_id_post)) - .route("/access_profile", get(|| async { "TODO" })) - .route("/access_profile/:id", get(|| async { "TODO" })) - .route("/access_profile/:id/_attr/:attr", get(|| async { "TODO" })) - .route("/auth", post(auth)) - .route("/auth/valid", get(auth_valid)) - .route("/logout", get(logout)) - .route("/reauth", post(reauth)) + .route("/v1/recycle_bin", get(recycle_bin_get)) + .route("/v1/recycle_bin/:id", get(recycle_bin_id_get)) .route( - "/sync_account", + "/v1/recycle_bin/:id/_revive", + post(recycle_bin_revive_id_post), + ) + .route("/v1/access_profile", get(|| async { "TODO" })) + .route("/v1/access_profile/:id", get(|| async { "TODO" })) + .route( + "/v1/access_profile/:id/_attr/:attr", + get(|| async { "TODO" }), + ) + .route("/v1/auth", post(auth)) + .route("/v1/auth/valid", get(auth_valid)) + .route("/v1/logout", get(logout)) + .route("/v1/reauth", post(reauth)) + .route( + "/v1/sync_account", get(sync_account_get).post(sync_account_post), ) .route( - "/sync_account/", + "/v1/sync_account/", get(sync_account_get).post(sync_account_post), ) .route( - "/sync_account/:id", + "/v1/sync_account/:id", get(sync_account_id_get).patch(sync_account_id_patch), ) .route( - "/sync_account/:id/_attr/:attr", + "/v1/sync_account/:id/_attr/:attr", get(sync_account_id_get_attr).put(sync_account_id_put_attr), ) .route( - "/sync_account/:id/_finalise", + "/v1/sync_account/:id/_finalise", get(sync_account_id_get_finalise), ) .route( - "/sync_account/:id/_terminate", + "/v1/sync_account/:id/_terminate", get(sync_account_id_get_terminate), ) .route( - "/sync_account/:id/_sync_token", - // .get(&mut sync_account_token_get) + "/v1/sync_account/:id/_sync_token", post(sync_account_token_post).delete(sync_account_token_delete), ) .with_state(state) diff --git a/server/core/src/https/v1_scim.rs b/server/core/src/https/v1_scim.rs index 20b339217..babf50249 100644 --- a/server/core/src/https/v1_scim.rs +++ b/server/core/src/https/v1_scim.rs @@ -254,6 +254,6 @@ pub fn scim_route_setup() -> Router { // // POST Send a sync update // - .route("/v1/Sync", post(scim_sync_post).get(scim_sync_get)) - .route("/v1/Sink", get(scim_sink_get)) + .route("/scim/v1/Sync", post(scim_sync_post).get(scim_sync_get)) + .route("/scim/v1/Sink", get(scim_sink_get)) } diff --git a/server/core/src/lib.rs b/server/core/src/lib.rs index b45f0437d..0adda609e 100644 --- a/server/core/src/lib.rs +++ b/server/core/src/lib.rs @@ -762,7 +762,7 @@ pub async fn create_server_core( } }; - let cookie_key: [u8; 32] = idms.get_cookie_key(); + let cookie_key: [u8; 64] = idms.get_cookie_key(); // Any pre-start tasks here. match &config.integration_test_config { diff --git a/server/daemon/src/main.rs b/server/daemon/src/main.rs index 29668700f..c93bb92ce 100644 --- a/server/daemon/src/main.rs +++ b/server/daemon/src/main.rs @@ -126,21 +126,15 @@ async fn main() -> ExitCode { } }; - // if they specified it in the environment then that overrides everything - let log_filter = match EnvFilter::try_from_default_env() { - Ok(val) => val, - Err(_e) => { - // we couldn't get it from the env, so we'll try the config file! - match sconfig.as_ref() { - Some(val) => { - let tmp = val.log_level.clone(); - tmp.unwrap_or_default() - } - None => LogLevel::Info, - } - .into() + // We only allow config file for log level now. + let log_filter: EnvFilter = match sconfig.as_ref() { + Some(val) => { + let tmp = val.log_level.clone(); + tmp.unwrap_or_default() } - }; + None => LogLevel::Info, + } + .into(); // TODO: only send to stderr when we're not in a TTY tracing_forest::worker_task() diff --git a/server/lib/src/constants/entries.rs b/server/lib/src/constants/entries.rs index 12a1db4ce..867a83aed 100644 --- a/server/lib/src/constants/entries.rs +++ b/server/lib/src/constants/entries.rs @@ -585,7 +585,7 @@ lazy_static! { "description", Value::new_utf8s("System (local) info and metadata object.") ), - ("version", Value::Uint32(12)) + ("version", Value::Uint32(13)) ); } diff --git a/server/lib/src/idm/server.rs b/server/lib/src/idm/server.rs index 790d1cc81..992f681e0 100644 --- a/server/lib/src/idm/server.rs +++ b/server/lib/src/idm/server.rs @@ -66,7 +66,7 @@ pub struct DomainKeys { pub(crate) uat_jwt_signer: JwsSigner, pub(crate) uat_jwt_validator: JwsValidator, pub(crate) token_enc_key: Fernet, - pub(crate) cookie_key: [u8; 32], + pub(crate) cookie_key: [u8; 64], } pub struct IdmServer { @@ -262,7 +262,7 @@ impl IdmServer { )) } - pub fn get_cookie_key(&self) -> [u8; 32] { + pub fn get_cookie_key(&self) -> [u8; 64] { self.domain_keys.read().cookie_key } diff --git a/server/lib/src/plugins/domain.rs b/server/lib/src/plugins/domain.rs index 604be85ae..76ba7db4f 100644 --- a/server/lib/src/plugins/domain.rs +++ b/server/lib/src/plugins/domain.rs @@ -129,7 +129,7 @@ impl Domain { if !e.attribute_pres("private_cookie_key") { security_info!("regenerating domain cookie key"); - let mut key = [0; 32]; + let mut key = [0; 64]; let mut rng = StdRng::from_entropy(); rng.fill(&mut key); let v = Value::new_privatebinary(&key); diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index a06f6824c..b5d17d5ca 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -99,6 +99,10 @@ impl QueryServer { if system_info_version < 12 { write_txn.migrate_11_to_12()?; } + + if system_info_version < 13 { + write_txn.migrate_12_to_13()?; + } } write_txn.reload()?; @@ -391,6 +395,19 @@ impl<'a> QueryServerWriteTransaction<'a> { self.internal_apply_writable(mod_candidates) } + #[instrument(level = "debug", skip_all)] + pub fn migrate_12_to_13(&mut self) -> Result<(), OperationError> { + admin_warn!("starting 12 to 13 migration."); + let filter = filter!(f_and!([ + f_eq("class", PVCLASS_DOMAIN_INFO.clone()), + f_eq("uuid", PVUUID_DOMAIN_INFO.clone()), + ])); + // Delete the existing cookie key to trigger a regeneration. + let modlist = ModifyList::new_purge("private_cookie_key"); + self.internal_modify(&filter, &modlist) + // Complete + } + #[instrument(level = "debug", skip_all)] pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> { admin_debug!("initialise_schema_core -> start ..."); diff --git a/server/lib/src/server/mod.rs b/server/lib/src/server/mod.rs index 0e9c8ee09..3ef906007 100644 --- a/server/lib/src/server/mod.rs +++ b/server/lib/src/server/mod.rs @@ -763,12 +763,12 @@ pub trait QueryServerTransaction<'a> { }) } - fn get_domain_cookie_key(&mut self) -> Result<[u8; 32], OperationError> { + fn get_domain_cookie_key(&mut self) -> Result<[u8; 64], OperationError> { self.internal_search_uuid(UUID_DOMAIN_INFO) .and_then(|e| { e.get_ava_single_private_binary("private_cookie_key") .and_then(|s| { - let mut x = [0; 32]; + let mut x = [0; 64]; if s.len() == x.len() { x.copy_from_slice(s); Some(x)