diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index c6b55a9..9e42053 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -90,6 +90,27 @@ jobs: - name: Test Console workspace run: just test console + - name: Apply migrations to the disposable CI database + run: cargo run -p grass-control-api -- --config "$RUNNER_TEMP/grass-migration.toml" migrate + env: + GWAPI_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres + + - name: Verify certificate database columns and constraints + run: >- + cargo test -p grass-control-api + infra::database::migration::m20260910_000032_managed_certificates::tests::managed_certificate_schema_matches_signed_lifecycle_and_ack_protocol + -- --ignored --exact + env: + GRASS_TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres + + - name: Verify regional ingress migration upgrade and rollback + run: >- + cargo test -p grass-control-api + infra::database::migrate::tests::postgres_regional_ingress_schema_matches_domain_and_is_reversible + -- --ignored --exact + env: + GRASS_TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres + - name: Check project license metadata run: just license-check diff --git a/Cargo.lock b/Cargo.lock index 2bb17b2..b661762 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -320,6 +320,73 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive 0.5.1", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive 0.6.0", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -543,6 +610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] @@ -761,7 +829,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-util", "tower-service", @@ -1257,6 +1325,34 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs 0.6.2", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs 0.7.2", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.8" @@ -1719,7 +1815,7 @@ version = "0.1.0" dependencies = [ "config", "serde", - "thiserror", + "thiserror 2.0.18", "toml 0.8.23", ] @@ -1745,21 +1841,26 @@ dependencies = [ "grass-token", "grass-validator", "hex", + "hyper-util", "image", + "instant-acme", "jsonwebtoken", "lettre", "mime_guess", "object_store", "rand 0.8.6", + "rcgen", "reqwest 0.12.28", "ring", + "rustls", + "rustls-pemfile", "sea-orm", "sea-orm-migration", "serde", "serde_json", "sha2", "subtle", - "thiserror", + "thiserror 2.0.18", "time", "tokio", "tokio-tungstenite 0.26.2", @@ -1774,6 +1875,7 @@ dependencies = [ "uuid", "walkdir", "webp", + "x509-parser 0.16.0", ] [[package]] @@ -1784,7 +1886,7 @@ dependencies = [ "base64 0.22.1", "ring", "serde", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1792,7 +1894,7 @@ name = "grass-git-source" version = "0.1.0" dependencies = [ "serde", - "thiserror", + "thiserror 2.0.18", "url", ] @@ -1820,7 +1922,8 @@ dependencies = [ "subtle", "sysinfo", "tar", - "thiserror", + "tempfile", + "thiserror 2.0.18", "tokio", "tokio-tungstenite 0.26.2", "tokio-util", @@ -2110,6 +2213,8 @@ dependencies = [ "hyper", "hyper-util", "rustls", + "rustls-native-certs", + "rustls-platform-verifier", "tokio", "tokio-rustls", "tower-service", @@ -2348,6 +2453,32 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "instant-acme" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f05ad37c421b962354c358d347d4a6130151df9407978372d3ad7f0c8f71a64" +dependencies = [ + "async-trait", + "aws-lc-rs", + "base64 0.22.1", + "bytes", + "http", + "http-body", + "http-body-util", + "httpdate", + "hyper", + "hyper-rustls", + "hyper-util", + "rcgen", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -2396,7 +2527,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.18", "walkdir", "windows-link 0.2.1", ] @@ -2512,7 +2643,7 @@ dependencies = [ "httpdate", "idna", "mime", - "nom", + "nom 8.0.0", "percent-encoding", "quoted_printable", "rustls", @@ -2737,6 +2868,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2813,6 +2950,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom" version = "8.0.0" @@ -2962,7 +3109,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "url", @@ -2972,6 +3119,24 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs 0.6.2", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs 0.7.2", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -3339,7 +3504,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.6.3", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -3362,7 +3527,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -3500,6 +3665,21 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rcgen" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +dependencies = [ + "aws-lc-rs", + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser 0.18.1", + "yasna", +] + [[package]] name = "redis" version = "0.31.0" @@ -3688,7 +3868,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -3807,6 +3987,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "1.1.4" @@ -3848,6 +4037,15 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -3894,7 +4092,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -3997,7 +4195,7 @@ dependencies = [ "serde_json", "sqlx", "strum", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "url", @@ -4012,7 +4210,7 @@ checksum = "5c2eee8405f16c1f337fe3a83389361caea83c928d14dbd666a480407072c365" dependencies = [ "arrow", "sea-query", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -4090,7 +4288,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -4369,7 +4567,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 2.0.18", "time", ] @@ -4476,7 +4674,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror", + "thiserror 2.0.18", "time", "tokio", "tokio-stream", @@ -4563,7 +4761,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "uuid", @@ -4604,7 +4802,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "uuid", @@ -4631,7 +4829,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "url", @@ -4769,13 +4967,46 @@ dependencies = [ "xattr", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -5199,7 +5430,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1", - "thiserror", + "thiserror 2.0.18", "utf-8", ] @@ -5216,7 +5447,7 @@ dependencies = [ "log", "rand 0.9.5", "sha1", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -5264,6 +5495,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -6037,6 +6274,42 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs 0.6.2", + "data-encoding", + "der-parser 9.0.0", + "lazy_static", + "nom 7.1.3", + "oid-registry 0.7.1", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs 0.7.2", + "aws-lc-rs", + "data-encoding", + "der-parser 10.0.0", + "lazy_static", + "nom 7.1.3", + "oid-registry 0.8.1", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "xattr" version = "1.6.1" @@ -6053,6 +6326,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.2" @@ -6169,7 +6451,7 @@ dependencies = [ "flate2", "indexmap 2.14.0", "memchr", - "thiserror", + "thiserror 2.0.18", "zopfli", ] diff --git a/Cargo.toml b/Cargo.toml index 4e54e23..4545b35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ bollard = "0.19" tar = "0.4" futures-util = "0.3" object_store = { version = "0.14.1", default-features = false, features = ["fs", "aws"] } +instant-acme = { version = "0.8.5", default-features = true, features = ["rcgen"] } grass-archive = { path = "crates/archive" } grass-assets = { path = "crates/assets" } diff --git a/apps/console/src/components/certificate-import-dialog.tsx b/apps/console/src/components/certificate-import-dialog.tsx new file mode 100644 index 0000000..4b69af5 --- /dev/null +++ b/apps/console/src/components/certificate-import-dialog.tsx @@ -0,0 +1,110 @@ +import { useMutation } from "@tanstack/react-query"; +import { useId, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field"; +import { Textarea } from "@/components/ui/textarea"; + +export function CertificateImportDialog({ + hostname, + onImport, + onImported, +}: { + hostname: string; + onImport: (input: { certificate_pem: string; private_key_pem: string }) => Promise; + onImported: () => void; +}) { + const id = useId(); + const [open, setOpen] = useState(false); + const [certificate, setCertificate] = useState(""); + const [privateKey, setPrivateKey] = useState(""); + const clear = () => { + setCertificate(""); + setPrivateKey(""); + }; + const mutation = useMutation({ + mutationFn: () => onImport({ certificate_pem: certificate, private_key_pem: privateKey }), + onSuccess: () => { + clear(); + setOpen(false); + onImported(); + }, + }); + return ( + { + setOpen(value); + if (!value) clear(); + }} + > + + + + + + Import certificate for {hostname} + + The certificate must cover this hostname and match its private key. Imported + certificates are renewed manually. + + +
{ + event.preventDefault(); + mutation.mutate(); + }} + > + + + Certificate chain (PEM) +