fixed #1356 - generate config schema (#1357)

This commit is contained in:
Eugene 2025-06-03 00:37:25 +02:00 committed by GitHub
parent 33803f1a30
commit 331af972bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 597 additions and 39 deletions

View file

@ -5,7 +5,7 @@ permissions:
on: [push, pull_request]
jobs:
Build:
build:
strategy:
matrix:
include:
@ -128,3 +128,27 @@ jobs:
generate_release_notes: true
files: dist/*
token: ${{ secrets.GITHUB_TOKEN }}
config-schema:
name: Config schema check
runs-on: ubuntu-24.04
steps:
- name: Setup
run: |
sudo apt update
sudo apt install --no-install-recommends -y libssl-dev pkg-config
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Install tools
run: |
cargo install just
- name: Ensure there are no changes in config schema
run: |
mkdir warpgate-web/dist
just config-schema
git diff --exit-code config-schema.json

59
Cargo.lock generated
View file

@ -3470,6 +3470,26 @@ dependencies = [
"bitflags 2.8.0",
]
[[package]]
name = "ref-cast"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "regex"
version = "1.11.1"
@ -3972,6 +3992,31 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "schemars"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
dependencies = [
"dyn-clone",
"ref-cast",
"schemars_derive",
"serde",
"serde_json",
]
[[package]]
name = "schemars_derive"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5016d94c77c6d32f0b8e08b781f7dc8a90c2007d4e77472cc2807bc10a8438fe"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.98",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -4230,6 +4275,17 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "serde_derive_internals"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "serde_json"
version = "1.0.138"
@ -5396,6 +5452,7 @@ dependencies = [
"notify",
"rcgen",
"rustls 0.23.22",
"schemars",
"sd-notify",
"sea-orm",
"serde_json",
@ -5469,6 +5526,7 @@ dependencies = [
"rustls 0.23.22",
"rustls-native-certs",
"rustls-pemfile 1.0.4",
"schemars",
"sea-orm",
"serde",
"serde_json",
@ -5690,6 +5748,7 @@ dependencies = [
"jsonwebtoken",
"once_cell",
"openidconnect",
"schemars",
"serde",
"serde_json",
"thiserror 1.0.69",

View file

@ -46,6 +46,7 @@ poem = { version = "3.1", features = [
password-hash = { version = "0.4", features = ["std"] }
delegate = "0.13"
tracing = "0.1"
schemars = "0.9.0"
[profile.release]
lto = true

459
config-schema.json Normal file
View file

@ -0,0 +1,459 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "WarpgateConfigStore",
"type": "object",
"properties": {
"database_url": {
"type": "string",
"default": "sqlite:data/db"
},
"external_host": {
"type": [
"string",
"null"
],
"default": null
},
"http": {
"$ref": "#/$defs/HttpConfig",
"default": {
"certificate": "",
"cookie_max_age": "1day",
"enable": false,
"external_port": null,
"key": "",
"listen": "[::]:8888",
"session_max_age": "30m",
"trust_x_forwarded_headers": false
}
},
"log": {
"$ref": "#/$defs/LogConfig",
"default": {
"retention": "7days",
"send_to": null
}
},
"mysql": {
"$ref": "#/$defs/MySqlConfig",
"default": {
"certificate": "",
"enable": false,
"external_port": null,
"key": "",
"listen": "[::]:33306"
}
},
"postgres": {
"$ref": "#/$defs/PostgresConfig",
"default": {
"certificate": "",
"enable": false,
"external_port": null,
"key": "",
"listen": "[::]:55432"
}
},
"recordings": {
"$ref": "#/$defs/RecordingsConfig",
"default": {
"enable": false,
"path": "./data/recordings"
}
},
"ssh": {
"$ref": "#/$defs/SshConfig",
"default": {
"enable": false,
"external_port": null,
"host_key_verification": "prompt",
"inactivity_timeout": "5m",
"keepalive_interval": null,
"keys": "./data/keys",
"listen": "[::]:2222"
}
},
"sso_providers": {
"type": "array",
"default": [],
"items": {
"$ref": "#/$defs/SsoProviderConfig"
}
}
},
"$defs": {
"Duration": {
"type": "object",
"properties": {
"nanos": {
"type": "integer",
"format": "uint32",
"minimum": 0
},
"secs": {
"type": "integer",
"format": "uint64",
"minimum": 0
}
},
"required": [
"secs",
"nanos"
]
},
"HttpConfig": {
"type": "object",
"properties": {
"certificate": {
"type": "string",
"default": ""
},
"cookie_max_age": {
"type": "string",
"default": "1day"
},
"enable": {
"type": "boolean",
"default": false
},
"external_port": {
"type": [
"integer",
"null"
],
"format": "uint16",
"default": null,
"maximum": 65535,
"minimum": 0
},
"key": {
"type": "string",
"default": ""
},
"listen": {
"$ref": "#/$defs/ListenEndpoint",
"default": "[::]:8888"
},
"session_max_age": {
"type": "string",
"default": "30m"
},
"trust_x_forwarded_headers": {
"type": "boolean",
"default": false
}
}
},
"ListenEndpoint": {
"type": "string"
},
"LogConfig": {
"type": "object",
"properties": {
"retention": {
"type": "string",
"default": "7days"
},
"send_to": {
"type": [
"string",
"null"
],
"default": null
}
}
},
"MySqlConfig": {
"type": "object",
"properties": {
"certificate": {
"type": "string",
"default": ""
},
"enable": {
"type": "boolean",
"default": false
},
"external_port": {
"type": [
"integer",
"null"
],
"format": "uint16",
"default": null,
"maximum": 65535,
"minimum": 0
},
"key": {
"type": "string",
"default": ""
},
"listen": {
"$ref": "#/$defs/ListenEndpoint",
"default": "[::]:33306"
}
}
},
"PostgresConfig": {
"type": "object",
"properties": {
"certificate": {
"type": "string",
"default": ""
},
"enable": {
"type": "boolean",
"default": false
},
"external_port": {
"type": [
"integer",
"null"
],
"format": "uint16",
"default": null,
"maximum": 65535,
"minimum": 0
},
"key": {
"type": "string",
"default": ""
},
"listen": {
"$ref": "#/$defs/ListenEndpoint",
"default": "[::]:55432"
}
}
},
"RecordingsConfig": {
"type": "object",
"properties": {
"enable": {
"type": "boolean",
"default": false
},
"path": {
"type": "string",
"default": "./data/recordings"
}
}
},
"SshConfig": {
"type": "object",
"properties": {
"enable": {
"type": "boolean",
"default": false
},
"external_port": {
"type": [
"integer",
"null"
],
"format": "uint16",
"default": null,
"maximum": 65535,
"minimum": 0
},
"host_key_verification": {
"$ref": "#/$defs/SshHostKeyVerificationMode",
"default": "prompt"
},
"inactivity_timeout": {
"type": "string",
"default": "5m"
},
"keepalive_interval": {
"anyOf": [
{
"$ref": "#/$defs/Duration"
},
{
"type": "null"
}
],
"default": null
},
"keys": {
"type": "string",
"default": "./data/keys"
},
"listen": {
"$ref": "#/$defs/ListenEndpoint",
"default": "[::]:2222"
}
}
},
"SshHostKeyVerificationMode": {
"type": "string",
"enum": [
"prompt",
"auto_accept",
"auto_reject"
]
},
"SsoInternalProviderConfig": {
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "google"
},
"client_id": {
"type": "string"
},
"client_secret": {
"type": "string"
}
},
"required": [
"type",
"client_id",
"client_secret"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "apple"
},
"client_id": {
"type": "string"
},
"client_secret": {
"type": "string"
},
"key_id": {
"type": "string"
},
"team_id": {
"type": "string"
}
},
"required": [
"type",
"client_id",
"client_secret",
"key_id",
"team_id"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "azure"
},
"client_id": {
"type": "string"
},
"client_secret": {
"type": "string"
},
"tenant": {
"type": "string"
}
},
"required": [
"type",
"client_id",
"client_secret",
"tenant"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "custom"
},
"additional_trusted_audiences": {
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"client_id": {
"type": "string"
},
"client_secret": {
"type": "string"
},
"issuer_url": {
"type": "string"
},
"role_mappings": {
"type": [
"object",
"null"
],
"additionalProperties": {
"type": "string"
}
},
"scopes": {
"type": "array",
"items": {
"type": "string"
}
},
"trust_unknown_audiences": {
"type": "boolean",
"default": false
}
},
"required": [
"type",
"client_id",
"client_secret",
"issuer_url",
"scopes"
]
}
]
},
"SsoProviderConfig": {
"type": "object",
"properties": {
"auto_create_users": {
"type": "boolean",
"default": false
},
"label": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
},
"provider": {
"$ref": "#/$defs/SsoInternalProviderConfig"
},
"return_domain_whitelist": {
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"required": [
"name",
"provider"
]
}
}
}

View file

@ -36,6 +36,9 @@ openapi-all:
openapi:
cd warpgate-web && npm run openapi:client:admin && npm run openapi:client:gateway
config-schema:
cargo run -p warpgate-common --bin config-schema > config-schema.json
cleanup: (fix "--allow-dirty") (clippy "--fix" "--allow-dirty") fmt svelte-check lint
udeps:

View file

@ -4,6 +4,10 @@ license = "Apache-2.0"
name = "warpgate-common"
version = "0.14.0"
[[bin]]
name = "config-schema"
path = "src/config_schema.rs"
[dependencies]
anyhow = "1.0"
argon2 = "0.5"
@ -45,3 +49,4 @@ rustls-pemfile = "1.0"
webpki = "0.22"
tokio-stream.workspace = true
git-version = "0.3.9"
schemars.workspace = true

View file

@ -8,6 +8,7 @@ use std::time::Duration;
use defaults::*;
use poem::http::uri;
use poem_openapi::{Object, Union};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub use target::*;
use tracing::warn;
@ -160,7 +161,7 @@ pub struct Role {
pub description: String,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Copy)]
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Copy, JsonSchema)]
pub enum SshHostKeyVerificationMode {
#[serde(rename = "prompt")]
#[default]
@ -171,7 +172,7 @@ pub enum SshHostKeyVerificationMode {
AutoReject,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
pub struct SshConfig {
#[serde(default = "_default_false")]
pub enable: bool,
@ -189,6 +190,7 @@ pub struct SshConfig {
pub host_key_verification: SshHostKeyVerificationMode,
#[serde(default = "_default_ssh_inactivity_timeout", with = "humantime_serde")]
#[schemars(with = "String")]
pub inactivity_timeout: Duration,
#[serde(default)]
@ -215,7 +217,7 @@ impl SshConfig {
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
pub struct HttpConfig {
#[serde(default = "_default_false")]
pub enable: bool,
@ -236,9 +238,11 @@ pub struct HttpConfig {
pub trust_x_forwarded_headers: bool,
#[serde(default = "_default_session_max_age", with = "humantime_serde")]
#[schemars(with = "String")]
pub session_max_age: Duration,
#[serde(default = "_default_cookie_max_age", with = "humantime_serde")]
#[schemars(with = "String")]
pub cookie_max_age: Duration,
}
@ -263,7 +267,7 @@ impl HttpConfig {
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
pub struct MySqlConfig {
#[serde(default = "_default_false")]
pub enable: bool,
@ -299,7 +303,7 @@ impl MySqlConfig {
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
pub struct PostgresConfig {
#[serde(default = "_default_false")]
pub enable: bool,
@ -335,7 +339,7 @@ impl PostgresConfig {
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
pub struct RecordingsConfig {
#[serde(default = "_default_false")]
pub enable: bool,
@ -353,9 +357,10 @@ impl Default for RecordingsConfig {
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
pub struct LogConfig {
#[serde(default = "_default_retention", with = "humantime_serde")]
#[schemars(with = "String")]
pub retention: Duration,
#[serde(default)]
@ -371,16 +376,7 @@ impl Default for LogConfig {
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
pub enum ConfigProviderKind {
#[serde(rename = "file")]
File,
#[serde(rename = "database")]
#[default]
Database,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
pub struct WarpgateConfigStore {
#[serde(default)]
pub sso_providers: Vec<SsoProviderConfig>,
@ -392,6 +388,7 @@ pub struct WarpgateConfigStore {
pub external_host: Option<String>,
#[serde(default = "_default_database_url")]
#[schemars(with = "String")]
pub database_url: Secret<String>,
#[serde(default)]
@ -408,9 +405,6 @@ pub struct WarpgateConfigStore {
#[serde(default)]
pub log: LogConfig,
#[serde(default)]
pub config_provider: ConfigProviderKind,
}
impl Default for WarpgateConfigStore {
@ -425,7 +419,6 @@ impl Default for WarpgateConfigStore {
mysql: <_>::default(),
postgres: <_>::default(),
log: <_>::default(),
config_provider: <_>::default(),
}
}
}

View file

@ -0,0 +1,7 @@
use schemars::schema_for;
#[allow(clippy::unwrap_used)]
fn main() {
let schema = schema_for!(warpgate_common::WarpgateConfigStore);
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}

View file

@ -5,13 +5,14 @@ use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs};
use futures::stream::{iter, FuturesUnordered};
use futures::{Stream, StreamExt, TryStreamExt};
use poem::listener::Listener;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tokio::net::{TcpListener, TcpStream};
use tokio_stream::wrappers::TcpListenerStream;
use crate::WarpgateError;
#[derive(Clone)]
#[derive(Clone, JsonSchema)]
pub struct ListenEndpoint(SocketAddr);
impl ListenEndpoint {

View file

@ -4,21 +4,19 @@ use std::time::Duration;
use anyhow::Result;
use sea_orm::DatabaseConnection;
use tokio::sync::Mutex;
use warpgate_common::{ConfigProviderKind, WarpgateConfig};
use warpgate_common::WarpgateConfig;
use crate::db::{connect_to_db, populate_db};
use crate::recordings::SessionRecordings;
use crate::{AuthStateStore, ConfigProviderEnum, DatabaseConfigProvider, State};
type ConfigProviderArc = Arc<Mutex<ConfigProviderEnum>>;
#[derive(Clone)]
pub struct Services {
pub db: Arc<Mutex<DatabaseConnection>>,
pub recordings: Arc<Mutex<SessionRecordings>>,
pub config: Arc<Mutex<WarpgateConfig>>,
pub state: Arc<Mutex<State>>,
pub config_provider: ConfigProviderArc,
pub config_provider: Arc<Mutex<ConfigProviderEnum>>,
pub auth_state_store: Arc<Mutex<AuthStateStore>>,
pub admin_token: Arc<Mutex<Option<String>>>,
}
@ -32,18 +30,9 @@ impl Services {
let recordings = SessionRecordings::new(db.clone(), &config)?;
let recordings = Arc::new(Mutex::new(recordings));
let provider = config.store.config_provider.clone();
let config = Arc::new(Mutex::new(config));
let config_provider = match provider {
ConfigProviderKind::File => {
anyhow::bail!("File based config provider in no longer supported");
}
ConfigProviderKind::Database => {
Arc::new(Mutex::new(DatabaseConfigProvider::new(&db).await.into()))
as ConfigProviderArc
}
};
let config_provider = Arc::new(Mutex::new(DatabaseConfigProvider::new(&db).await.into()));
let auth_state_store = Arc::new(Mutex::new(AuthStateStore::new(config_provider.clone())));

View file

@ -19,3 +19,4 @@ once_cell = "1.17"
jsonwebtoken = "9"
data-encoding.workspace = true
futures.workspace = true
schemars.workspace = true

View file

@ -4,6 +4,7 @@ use std::time::SystemTime;
use data_encoding::BASE64;
use once_cell::sync::Lazy;
use openidconnect::{AuthType, ClientId, ClientSecret, IssuerUrl};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::SsoError;
@ -16,7 +17,7 @@ pub static GOOGLE_ISSUER_URL: Lazy<IssuerUrl> =
pub static APPLE_ISSUER_URL: Lazy<IssuerUrl> =
Lazy::new(|| IssuerUrl::new("https://appleid.apple.com".to_string()).unwrap());
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct SsoProviderConfig {
pub name: String,
pub label: Option<String>,
@ -34,31 +35,40 @@ impl SsoProviderConfig {
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
pub enum SsoInternalProviderConfig {
#[serde(rename = "google")]
Google {
#[schemars(with = "String")]
client_id: ClientId,
#[schemars(with = "String")]
client_secret: ClientSecret,
},
#[serde(rename = "apple")]
Apple {
#[schemars(with = "String")]
client_id: ClientId,
#[schemars(with = "String")]
client_secret: ClientSecret,
key_id: String,
team_id: String,
},
#[serde(rename = "azure")]
Azure {
#[schemars(with = "String")]
client_id: ClientId,
#[schemars(with = "String")]
client_secret: ClientSecret,
tenant: String,
},
#[serde(rename = "custom")]
Custom {
#[schemars(with = "String")]
client_id: ClientId,
#[schemars(with = "String")]
client_secret: ClientSecret,
#[schemars(with = "String")]
issuer_url: IssuerUrl,
scopes: Vec<String>,
role_mappings: Option<HashMap<String, String>>,

View file

@ -39,6 +39,7 @@ warpgate-protocol-http = { version = "*", path = "../warpgate-protocol-http" }
warpgate-protocol-mysql = { version = "*", path = "../warpgate-protocol-mysql" }
warpgate-protocol-postgres = { version = "*", path = "../warpgate-protocol-postgres" }
warpgate-protocol-ssh = { version = "*", path = "../warpgate-protocol-ssh" }
schemars.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
sd-notify = "0.4"

View file

@ -293,6 +293,11 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
let yaml = serde_yaml::to_string(&store)?;
println!("{yaml}");
let yaml = format!(
"# Config generated in version {version}\n# yaml-language-server: $schema=https://raw.githubusercontent.com/warp-tech/warpgate/refs/heads/main/config-schema.json\n\n{yaml}",
version = warpgate_version()
);
File::create(&cli.config)?.write_all(yaml.as_bytes())?;
info!("Saved into {}", cli.config.display());