diff --git a/.github/workflows/core-protocol.yml b/.github/workflows/core-protocol.yml index 477afcbda..1973b34ef 100644 --- a/.github/workflows/core-protocol.yml +++ b/.github/workflows/core-protocol.yml @@ -140,6 +140,16 @@ jobs: LMM_MODEL_PROCESS_CORE_SHA256="$(sha256sum "$LMM_MODEL_PROCESS_CORE_BIN" | cut -d ' ' -f1)" LMM_MODEL_PROCESS_ROOT="$(mktemp -d /tmp/lmm-model-XXXXXXXX)" export LMM_MODEL_PROCESS_CORE_BIN LMM_MODEL_PROCESS_CORE_SHA256 LMM_MODEL_PROCESS_ROOT + # The non-ignored payment recovery case requires sealed Core, Go host + # and bootstrap binaries. The 302-second case stays ignored. + CGO_ENABLED=0 go test -C ../extensions -mod=readonly -c -o "$RUNNER_TEMP/lmm-payment-bootstrap" ./internal/modules/payments + LMM_PAYMENT_CORE_BIN="$LMM_MODEL_PROCESS_CORE_BIN" + LMM_PAYMENT_CORE_SHA256="$LMM_MODEL_PROCESS_CORE_SHA256" + LMM_PAYMENT_GO_BIN="$(realpath "$LMM_RELAY_EXTENSION_BIN")" + LMM_PAYMENT_GO_SHA256="$(sha256sum "$LMM_PAYMENT_GO_BIN" | cut -d ' ' -f1)" + LMM_PAYMENT_BOOTSTRAP_BIN="$(realpath "$RUNNER_TEMP/lmm-payment-bootstrap")" + LMM_PAYMENT_BOOTSTRAP_SHA256="$(sha256sum "$LMM_PAYMENT_BOOTSTRAP_BIN" | cut -d ' ' -f1)" + export LMM_PAYMENT_CORE_BIN LMM_PAYMENT_CORE_SHA256 LMM_PAYMENT_GO_BIN LMM_PAYMENT_GO_SHA256 LMM_PAYMENT_BOOTSTRAP_BIN LMM_PAYMENT_BOOTSTRAP_SHA256 cargo test --locked --all-targets --no-fail-fast 2>&1 | tee "$RUNNER_TEMP/core-tests.log" - name: Go contract and race tests if: ${{ !cancelled() }} diff --git a/deployment/postgres/grant-core-runtime.sql b/deployment/postgres/grant-core-runtime.sql index f5b0ce491..140f602a4 100644 --- a/deployment/postgres/grant-core-runtime.sql +++ b/deployment/postgres/grant-core-runtime.sql @@ -54,7 +54,11 @@ SELECT EXISTS ( \else \echo 'Refusing unsafe runtime role or login-enabled installation owner.' ROLLBACK; -\quit 1 +-- psql \quit ignores an exit status, so a refused script would otherwise +-- leave the process successful after this rollback. ON_ERROR_STOP is set above. +DO $$ BEGIN + RAISE EXCEPTION 'Refusing unsafe runtime role or login-enabled installation owner.'; +END $$; \endif REVOKE CREATE, TEMPORARY ON DATABASE :"core_database" FROM PUBLIC; diff --git a/services/core/tests/browser_sessions_independent.rs b/services/core/tests/browser_sessions_independent.rs index 9539725c5..03104273a 100644 --- a/services/core/tests/browser_sessions_independent.rs +++ b/services/core/tests/browser_sessions_independent.rs @@ -28,6 +28,20 @@ impl RuntimeRole { let database = owner.connect_options().get_database().unwrap().to_owned(); let mut admin = reqwest::Url::parse(&std::env::var("DATABASE_URL").unwrap()).unwrap(); assert_eq!(admin.host_str(), Some("127.0.0.1")); + // The official grant script refuses, and rolls back, unless the NOLOGIN + // installation owner already exists. That role is cluster-wide. + sqlx::raw_sql( + "DO $$ BEGIN \ + CREATE ROLE lmm_core_owner NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS; \ + EXCEPTION WHEN duplicate_object THEN \ + IF EXISTS (SELECT FROM pg_roles r WHERE r.rolname='lmm_core_owner' AND r.rolcanlogin) THEN \ + RAISE EXCEPTION 'installation owner must not log in'; \ + END IF; \ + END $$;", + ) + .execute(owner) + .await + .unwrap(); let grants = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("../../deployment/postgres/grant-core-runtime.sql"); let output = std::process::Command::new("/usr/bin/psql") @@ -55,7 +69,9 @@ impl RuntimeRole { .unwrap(); assert!( output.status.success(), - "official runtime grants failed before session behavior" + "official runtime grants failed before session behavior\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) ); admin.set_path(&database); admin.set_username(&name).unwrap(); diff --git a/services/core/tests/oauth2_pi_independent.rs b/services/core/tests/oauth2_pi_independent.rs index d203a76d4..a6dbfcea4 100644 --- a/services/core/tests/oauth2_pi_independent.rs +++ b/services/core/tests/oauth2_pi_independent.rs @@ -252,12 +252,20 @@ async fn revoked_expired_stream_retains_original_hold_until_authoritative_reconc pool: PgPool, ) { with_fixture(pool,true,|f|async move { - let token=f.token(100).await;let mut spec=f.spec("independent-crash",60);spec.lease_seconds=1;spec.maximum_seconds=1; + let token=f.token(100).await; + // A one-second lease is already stale before start once reserve and the + // following serializable transaction cross a clock second. Dispatch must + // succeed first; expiry is then the same explicit lease move used by the + // charging recovery tests. + let spec=f.spec("independent-crash",60); let held=f.reserve(&token.access_token,&spec,&[1;32]).await.unwrap(); assert!(f.charging.start_oauth(&f.issuer,&held.id,&[1;32]).await.unwrap()); f.issuer.revoke_for_client("lmm-pi",&token.access_token).await.unwrap(); sqlx::query("UPDATE core_identity.users SET active=false WHERE id=$1").bind(f.user).execute(&f.owner).await.unwrap(); - tokio::time::sleep(Duration::from_secs(2)).await; + let mut expiry=f.owner.begin().await.unwrap(); + sqlx::query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE").execute(&mut *expiry).await.unwrap(); + sqlx::query("UPDATE core_charging.requests SET lease_until=floor(extract(epoch FROM clock_timestamp()))::bigint-1,revision=revision+1 WHERE id=$1").bind(&held.id).execute(&mut *expiry).await.unwrap(); + expiry.commit().await.unwrap(); let recovered=f.charging.recover_expired(10).await.unwrap();assert_eq!(recovered.len(),1);let recovered=&recovered[0];assert_eq!(recovered.state,"reconcile");assert_eq!(recovered.oauth_source,held.oauth_source); assert_eq!(f.ledger.balance(f.account).await.unwrap().reserved_units,60); let usage=Usage{input:25,..Default::default()};