v3 : seconde relecture — une coquille ne touche ni chiffre, ni signe, ni relation ; retour arrière du déploiement
- Correction de coquille : un chiffre, un signe, un séparateur dans un nombre ou une relation changés (« >= » → « <= », « + » → « - », « 182.625 » → « 182 625 », « % » → « ‰ ») rendent la modification « de fond ». Testé dans le moteur et par l'API sur le vrai texte K1. - Déploiement : les conteneurs retirés de la racine qui tournaient sont notés ; si la suite échoue, l'étape « retablir » arrête le produit et les relance. Le produit est recréé à chaque déploiement (--force-recreate). - Cookies préfixés en https : __Host-ld_session, __Secure-ld_oauth (pas de cookie planté par un site voisin sous librodrome.org). - syft épinglé (v1.55.0) dans les deux pipelines : ce conteneur a le socket Docker. - Docs : limites connues assumées, groupe d'essai à deux voix, error.identity_kind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,17 @@ use trustin_access::{Identity, IdentityKind};
|
||||
|
||||
pub const SESSION_COOKIE: &str = "ld_session";
|
||||
const OAUTH_COOKIE: &str = "ld_oauth";
|
||||
|
||||
/// Over https, the prefixed names: a neighbouring site under the same parent
|
||||
/// domain cannot plant a `__Host-` cookie (session fixation), nor a
|
||||
/// `__Secure-` one without https.
|
||||
fn session_cookie(state: &AppState) -> &'static str {
|
||||
if state.cfg.secure_cookies() { "__Host-ld_session" } else { SESSION_COOKIE }
|
||||
}
|
||||
|
||||
fn oauth_cookie(state: &AppState) -> &'static str {
|
||||
if state.cfg.secure_cookies() { "__Secure-ld_oauth" } else { OAUTH_COOKIE }
|
||||
}
|
||||
const SESSION_DAYS: i64 = 30;
|
||||
const INVITATION_DAYS: i64 = 14;
|
||||
|
||||
@@ -83,7 +94,7 @@ impl FromRequestParts<AppState> for Viewer {
|
||||
type Rejection = AppError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
|
||||
let Some(token) = cookie_value(&parts.headers, SESSION_COOKIE) else {
|
||||
let Some(token) = cookie_value(&parts.headers, session_cookie(state)) else {
|
||||
return Ok(Self(None));
|
||||
};
|
||||
let row = sqlx::query_as::<_, IdentityRow>(&format!(
|
||||
@@ -124,7 +135,7 @@ async fn open_session(state: &AppState, identity_id: &str) -> AppResult<HeaderVa
|
||||
.bind(identity_id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
Ok(cookie(state, SESSION_COOKIE, &token, "/", SESSION_DAYS * 86_400))
|
||||
Ok(cookie(state, session_cookie(state), &token, "/", SESSION_DAYS * 86_400))
|
||||
}
|
||||
|
||||
pub async fn load_identity(state: &AppState, id: &str) -> AppResult<IdentityRow> {
|
||||
@@ -212,7 +223,7 @@ pub async fn g1_start(State(state): State<AppState>, Query(q): Query<StartQuery>
|
||||
.append_pair("redirect_uri", &state.cfg.connect_callback())
|
||||
.append_pair("state", &csrf);
|
||||
let stash = format!("{csrf}.{}", url::form_urlencoded::byte_serialize(back.as_bytes()).collect::<String>());
|
||||
Ok(redirect(url.as_str(), vec![cookie(&state, OAUTH_COOKIE, &stash, "/api/auth/g1", 600)]))
|
||||
Ok(redirect(url.as_str(), vec![cookie(&state, oauth_cookie(&state), &stash, "/api/auth/g1", 600)]))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -243,8 +254,8 @@ pub async fn g1_callback(
|
||||
headers: HeaderMap,
|
||||
Query(q): Query<CallbackQuery>,
|
||||
) -> Response {
|
||||
let clear = cookie(&state, OAUTH_COOKIE, "", "/api/auth/g1", 0);
|
||||
let stash = cookie_value(&headers, OAUTH_COOKIE).unwrap_or_default();
|
||||
let clear = cookie(&state, oauth_cookie(&state), "", "/api/auth/g1", 0);
|
||||
let stash = cookie_value(&headers, oauth_cookie(&state)).unwrap_or_default();
|
||||
let (expected, back) = stash.split_once('.').unwrap_or(("", ""));
|
||||
let back =
|
||||
safe_return(url::form_urlencoded::parse(format!("p={back}").as_bytes()).next().map(|(_, v)| v).as_deref());
|
||||
@@ -460,14 +471,14 @@ pub async fn dev_login(State(state): State<AppState>, Json(body): Json<DevBody>)
|
||||
}
|
||||
|
||||
pub async fn logout(State(state): State<AppState>, headers: HeaderMap) -> AppResult<Response> {
|
||||
if let Some(token) = cookie_value(&headers, SESSION_COOKIE) {
|
||||
if let Some(token) = cookie_value(&headers, session_cookie(&state)) {
|
||||
sqlx::query("DELETE FROM sessions WHERE token_hash = ?")
|
||||
.bind(util::sha256_hex(&token))
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
}
|
||||
let mut res = Json(json!({ "me": null })).into_response();
|
||||
res.headers_mut().append(SET_COOKIE, cookie(&state, SESSION_COOKIE, "", "/", 0));
|
||||
res.headers_mut().append(SET_COOKIE, cookie(&state, session_cookie(&state), "", "/", 0));
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -522,6 +533,8 @@ mod tests {
|
||||
assert_eq!(safe_return(Some("//evil.example")), "/groupes");
|
||||
assert_eq!(safe_return(Some("https://evil.example")), "/groupes");
|
||||
assert_eq!(safe_return(Some("/\\evil")), "/groupes");
|
||||
assert_eq!(safe_return(Some("/\t/evil.example")), "/groupes");
|
||||
assert_eq!(safe_return(Some("/\n/evil.example")), "/groupes");
|
||||
assert_eq!(safe_return(None), "/groupes");
|
||||
}
|
||||
|
||||
|
||||
@@ -218,6 +218,18 @@ async fn editing_is_not_voting() {
|
||||
.await;
|
||||
assert_eq!(s, StatusCode::CONFLICT);
|
||||
assert_eq!(v["error"], "substantive_in_force");
|
||||
// Flipping a relation in a seed text is not a typo, even with the same letters.
|
||||
let k1 = unit(&doc, "K1")["proposals"][0].clone();
|
||||
let flipped = k1["body"].as_str().expect("body").replace(">= 5 certifications", "<= 5 certifications");
|
||||
let (s, v) = b
|
||||
.call(
|
||||
"PATCH",
|
||||
&format!("/api/proposals/{}", k1["id"].as_str().expect("id")),
|
||||
Some(json!({ "title": k1["title"], "body": flipped })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(s, StatusCode::CONFLICT, "{v}");
|
||||
assert_eq!(v["error"], "substantive_not_author");
|
||||
|
||||
// A member proposal: typo keeps votes, substance asks to reconfirm.
|
||||
let uid = unit(&doc, "E2")["id"].as_str().expect("u").to_owned();
|
||||
|
||||
Reference in New Issue
Block a user