mirror of
https://github.com/moghtech/komodo.git
synced 2025-12-05 19:17:36 -06:00
KL-2/3 Input validation for local auth, service users, api keys, and variables
This commit is contained in:
115
Cargo.lock
generated
115
Cargo.lock
generated
@@ -144,9 +144,9 @@ checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.33"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93c1f86859c1af3d514fa19e8323147ff10ea98684e6c7b307912509f50e67b2"
|
||||
checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473"
|
||||
dependencies = [
|
||||
"compression-codecs",
|
||||
"compression-core",
|
||||
@@ -193,9 +193,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "aws-config"
|
||||
version = "1.8.10"
|
||||
version = "1.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1856b1b48b65f71a4dd940b1c0931f9a7b646d4a924b9828ffefc1454714668a"
|
||||
checksum = "a0149602eeaf915158e14029ba0c78dedb8c08d554b024d54c8f239aab46511d"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -223,9 +223,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-credential-types"
|
||||
version = "1.2.9"
|
||||
version = "1.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86590e57ea40121d47d3f2e131bfd873dea15d78dc2f4604f4734537ad9e56c4"
|
||||
checksum = "b01c9521fa01558f750d183c8c68c81b0155b9d193a4ba7f84c36bd1b6d04a06"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -260,9 +260,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-runtime"
|
||||
version = "1.5.14"
|
||||
version = "1.5.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fe0fd441565b0b318c76e7206c8d1d0b0166b3e986cf30e890b61feb6192045"
|
||||
checksum = "7ce527fb7e53ba9626fc47824f25e256250556c40d8f81d27dd92aa38239d632"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-sigv4",
|
||||
@@ -284,9 +284,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ec2"
|
||||
version = "1.188.0"
|
||||
version = "1.194.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce23f4f9971b682019024aed0bf0fd971df7bf86519e41cf2f7ec7a631049395"
|
||||
checksum = "f55f9073b102788b24a8382fdadd27f5bf34d5ff4869eb88cdf251a455599e63"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -307,9 +307,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "1.89.0"
|
||||
version = "1.90.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9c1b1af02288f729e95b72bd17988c009aa72e26dcb59b3200f86d7aea726c9"
|
||||
checksum = "4f18e53542c522459e757f81e274783a78f8c81acdfc8d1522ee8a18b5fb1c66"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -329,9 +329,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ssooidc"
|
||||
version = "1.91.0"
|
||||
version = "1.92.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e8122301558dc7c6c68e878af918880b82ff41897a60c8c4e18e4dc4d93e9f1"
|
||||
checksum = "532f4d866012ffa724a4385c82e8dd0e59f0ca0e600f3f22d4c03b6824b34e4a"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -351,9 +351,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.92.0"
|
||||
version = "1.94.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c7808adcff8333eaa76a849e6de926c6ac1a1268b9fd6afe32de9c29ef29d2"
|
||||
checksum = "1be6fbbfa1a57724788853a623378223fe828fc4c09b146c992f0c95b6256174"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -867,7 +867,7 @@ dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"getrandom 0.3.3",
|
||||
"hex",
|
||||
"indexmap 2.12.0",
|
||||
"indexmap 2.12.1",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"rand 0.9.2",
|
||||
@@ -1025,9 +1025,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.52"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8"
|
||||
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -1035,9 +1035,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.52"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1"
|
||||
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1110,9 +1110,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
version = "0.4.32"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680dc087785c5230f8e8843e2e57ac7c1c90488b6a91b88caa265410568f441b"
|
||||
checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad"
|
||||
dependencies = [
|
||||
"compression-core",
|
||||
"flate2",
|
||||
@@ -1121,16 +1121,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "compression-core"
|
||||
version = "0.4.30"
|
||||
version = "0.4.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a9b614a5787ef0c8802a55766480563cb3a93b435898c422ed2a359cf811582"
|
||||
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "2.0.0-dev-91"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"indexmap 2.12.0",
|
||||
"indexmap 2.12.1",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1769,9 +1769,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "english-to-cron"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e26fb7377cbec9a94f60428e6e6afbe10c699a14639b4d3d4b67b25c0bbe0806"
|
||||
checksum = "3c3d16f6dc9dc43a9a2fd5bce09b6cf8df250dcf77cffdaa66be21c527e2d05c"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
@@ -2107,7 +2107,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"indexmap 2.12.0",
|
||||
"indexmap 2.12.1",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -2126,7 +2126,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http 1.3.1",
|
||||
"indexmap 2.12.0",
|
||||
"indexmap 2.12.1",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -2158,9 +2158,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "headers"
|
||||
@@ -2681,12 +2681,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.12.0"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
|
||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.0",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
@@ -2865,7 +2865,7 @@ dependencies = [
|
||||
"derive_variants",
|
||||
"envy",
|
||||
"futures-util",
|
||||
"indexmap 2.12.0",
|
||||
"indexmap 2.12.1",
|
||||
"ipnetwork",
|
||||
"mongo_indexed",
|
||||
"partial_derive2",
|
||||
@@ -2922,7 +2922,7 @@ dependencies = [
|
||||
"git",
|
||||
"hex",
|
||||
"hmac",
|
||||
"indexmap 2.12.0",
|
||||
"indexmap 2.12.1",
|
||||
"interpolate",
|
||||
"jsonwebtoken",
|
||||
"komodo_client",
|
||||
@@ -2957,6 +2957,7 @@ dependencies = [
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"validations",
|
||||
"wildcard",
|
||||
]
|
||||
|
||||
@@ -4653,7 +4654,7 @@ version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"indexmap 2.12.0",
|
||||
"indexmap 2.12.1",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
@@ -4734,7 +4735,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.12.0",
|
||||
"indexmap 2.12.1",
|
||||
"schemars 0.9.0",
|
||||
"schemars 1.0.4",
|
||||
"serde_core",
|
||||
@@ -4761,7 +4762,7 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f"
|
||||
dependencies = [
|
||||
"indexmap 2.12.0",
|
||||
"indexmap 2.12.1",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
@@ -5348,7 +5349,7 @@ version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
|
||||
dependencies = [
|
||||
"indexmap 2.12.0",
|
||||
"indexmap 2.12.1",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
@@ -5440,7 +5441,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"indexmap 2.12.0",
|
||||
"indexmap 2.12.1",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
"sync_wrapper",
|
||||
@@ -5453,9 +5454,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.6"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||
checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bytes",
|
||||
@@ -5493,9 +5494,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
@@ -5505,9 +5506,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.30"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5516,9 +5517,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
@@ -5566,9 +5567,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.20"
|
||||
version = "0.3.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
|
||||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||
dependencies = [
|
||||
"nu-ansi-term",
|
||||
"serde",
|
||||
@@ -5804,6 +5805,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validations"
|
||||
version = "2.0.0-dev-91"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bson",
|
||||
"regex",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
|
||||
21
Cargo.toml
21
Cargo.toml
@@ -26,6 +26,7 @@ environment_file = { path = "lib/environment_file" }
|
||||
environment = { path = "lib/environment" }
|
||||
interpolate = { path = "lib/interpolate" }
|
||||
secret_file = { path = "lib/secret_file" }
|
||||
validations = { path = "lib/validations" }
|
||||
formatting = { path = "lib/formatting" }
|
||||
transport = { path = "lib/transport" }
|
||||
database = { path = "lib/database" }
|
||||
@@ -64,13 +65,13 @@ arc-swap = "1.7.1"
|
||||
# SERVER
|
||||
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] }
|
||||
axum-extra = { version = "0.12.2", features = ["typed-header"] }
|
||||
tower-http = { version = "0.6.6", features = ["fs", "cors"] }
|
||||
tower-http = { version = "0.6.7", features = ["fs", "cors", "set-header"] }
|
||||
axum-server = { version = "0.7.3", features = ["tls-rustls"] }
|
||||
axum = { version = "0.8.7", features = ["ws", "json", "macros"] }
|
||||
|
||||
# SER/DE
|
||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||
indexmap = { version = "2.12.0", features = ["serde"] }
|
||||
indexmap = { version = "2.12.1", features = ["serde"] }
|
||||
serde = { version = "1.0.227", features = ["derive"] }
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
bson = { version = "2.15.0" } # must keep in sync with mongodb version
|
||||
@@ -87,14 +88,14 @@ thiserror = "2.0.17"
|
||||
# LOGGING
|
||||
opentelemetry-otlp = { version = "0.31.0", features = ["tls-roots", "reqwest-rustls"] }
|
||||
opentelemetry_sdk = { version = "0.31.0", features = ["rt-tokio"] }
|
||||
tracing-subscriber = { version = "0.3.20", features = ["json"] }
|
||||
tracing-subscriber = { version = "0.3.22", features = ["json"] }
|
||||
opentelemetry-semantic-conventions = "0.31.0"
|
||||
tracing-opentelemetry = "0.32.0"
|
||||
opentelemetry = "0.31.0"
|
||||
tracing = "0.1.41"
|
||||
tracing = "0.1.43"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.52", features = ["derive"] }
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
@@ -127,18 +128,18 @@ sysinfo = "0.37.1"
|
||||
shlex = "1.3.0"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.8.10"
|
||||
aws-sdk-ec2 = "1.188.0"
|
||||
aws-credential-types = "1.2.9"
|
||||
aws-config = "1.8.11"
|
||||
aws-sdk-ec2 = "1.194.0"
|
||||
aws-credential-types = "1.2.10"
|
||||
|
||||
## CRON
|
||||
english-to-cron = "0.1.6"
|
||||
english-to-cron = "0.1.7"
|
||||
chrono-tz = "0.10.4"
|
||||
chrono = "0.4.42"
|
||||
croner = "3.0.1"
|
||||
|
||||
# MISC
|
||||
async-compression = { version = "0.4.33", features = ["tokio", "gzip"] }
|
||||
async-compression = { version = "0.4.34", features = ["tokio", "gzip"] }
|
||||
derive_builder = "0.20.2"
|
||||
comfy-table = "7.2.1"
|
||||
typeshare = "1.0.4"
|
||||
|
||||
@@ -20,6 +20,7 @@ periphery_client.workspace = true
|
||||
environment_file.workspace = true
|
||||
interpolate.workspace = true
|
||||
secret_file.workspace = true
|
||||
validations.workspace = true
|
||||
formatting.workspace = true
|
||||
transport.workspace = true
|
||||
database.workspace = true
|
||||
|
||||
@@ -5,10 +5,9 @@ use hmac::{Hmac, Mac};
|
||||
use serde::Deserialize;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
listener::{ExtractBranch, VerifySecret},
|
||||
};
|
||||
use crate::config::core_config;
|
||||
|
||||
use super::{ExtractBranch, VerifySecret};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
listener::{ExtractBranch, VerifySecret},
|
||||
};
|
||||
use crate::config::core_config;
|
||||
|
||||
use super::{ExtractBranch, VerifySecret};
|
||||
|
||||
/// Listener implementation for Gitlab type API
|
||||
pub struct Gitlab;
|
||||
4
bin/core/src/api/listener/integrations/mod.rs
Normal file
4
bin/core/src/api/listener/integrations/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod github;
|
||||
pub mod gitlab;
|
||||
|
||||
use super::{ExtractBranch, VerifySecret};
|
||||
@@ -1,11 +1,70 @@
|
||||
use axum::{
|
||||
Router,
|
||||
http::{HeaderName, HeaderValue},
|
||||
routing::get,
|
||||
};
|
||||
use tower_http::{
|
||||
services::{ServeDir, ServeFile},
|
||||
set_header::SetResponseHeaderLayer,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{core_config, cors_layer},
|
||||
ts_client,
|
||||
};
|
||||
|
||||
pub mod auth;
|
||||
pub mod execute;
|
||||
pub mod read;
|
||||
pub mod terminal;
|
||||
pub mod user;
|
||||
pub mod write;
|
||||
|
||||
mod listener;
|
||||
mod terminal;
|
||||
mod ws;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Variant {
|
||||
variant: String,
|
||||
}
|
||||
|
||||
pub fn app() -> Router {
|
||||
let config = core_config();
|
||||
|
||||
// Setup static frontend services
|
||||
let frontend_path = &config.frontend_path;
|
||||
let frontend_index =
|
||||
ServeFile::new(format!("{frontend_path}/index.html"));
|
||||
let serve_frontend = ServeDir::new(frontend_path)
|
||||
.not_found_service(frontend_index.clone());
|
||||
|
||||
Router::new()
|
||||
.route("/version", get(|| async { env!("CARGO_PKG_VERSION") }))
|
||||
.nest("/auth", auth::router())
|
||||
.nest("/user", user::router())
|
||||
.nest("/read", read::router())
|
||||
.nest("/write", write::router())
|
||||
.nest("/execute", execute::router())
|
||||
.nest("/terminal", terminal::router())
|
||||
.nest("/listener", listener::router())
|
||||
.nest("/ws", ws::router())
|
||||
.nest("/client", ts_client::router())
|
||||
.fallback_service(serve_frontend)
|
||||
.layer(cors_layer())
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
HeaderName::from_static("x-content-type-options"),
|
||||
HeaderValue::from_static("nosniff"),
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
HeaderName::from_static("x-frame-options"),
|
||||
HeaderValue::from_static("DENY"),
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
HeaderName::from_static("x-xss-protection"),
|
||||
HeaderValue::from_static("1; mode=block"),
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
HeaderName::from_static("referrer-policy"),
|
||||
HeaderValue::from_static("strict-origin-when-cross-origin"),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -14,13 +14,16 @@ use komodo_client::{
|
||||
api::user::*,
|
||||
entities::{api_key::ApiKey, komodo_timestamp, user::User},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::AddStatusCodeError;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::helpers::validations::validate_api_key_name;
|
||||
use crate::{
|
||||
auth::auth_request, helpers::query::get_user, state::db_client,
|
||||
};
|
||||
@@ -94,6 +97,9 @@ impl Resolve<UserArgs> for PushRecentlyViewed {
|
||||
let user = get_user(&user.id).await?;
|
||||
|
||||
let (resource_type, id) = self.resource.extract_variant_id();
|
||||
|
||||
let field = format!("recents.{resource_type}");
|
||||
|
||||
let update = match user.recents.get(&resource_type) {
|
||||
Some(recents) => {
|
||||
let mut recents = recents
|
||||
@@ -101,13 +107,16 @@ impl Resolve<UserArgs> for PushRecentlyViewed {
|
||||
.filter(|_id| !id.eq(*_id))
|
||||
.take(RECENTLY_VIEWED_MAX - 1)
|
||||
.collect::<VecDeque<_>>();
|
||||
|
||||
recents.push_front(id);
|
||||
doc! { format!("recents.{resource_type}"): to_bson(&recents)? }
|
||||
|
||||
doc! { &field: to_bson(&recents)? }
|
||||
}
|
||||
None => {
|
||||
doc! { format!("recents.{resource_type}"): [id] }
|
||||
doc! { &field: [id] }
|
||||
}
|
||||
};
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().users,
|
||||
&user.id,
|
||||
@@ -115,9 +124,7 @@ impl Resolve<UserArgs> for PushRecentlyViewed {
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to update recents.{resource_type}")
|
||||
})?;
|
||||
.with_context(|| format!("Failed to update user '{field}'"))?;
|
||||
|
||||
Ok(PushRecentlyViewedResponse {})
|
||||
}
|
||||
@@ -137,7 +144,8 @@ impl Resolve<UserArgs> for SetLastSeenUpdate {
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to update user last_update_view")?;
|
||||
.context("Failed to update user 'last_update_view'")?;
|
||||
|
||||
Ok(SetLastSeenUpdateResponse {})
|
||||
}
|
||||
}
|
||||
@@ -157,10 +165,12 @@ impl Resolve<UserArgs> for CreateApiKey {
|
||||
) -> serror::Result<CreateApiKeyResponse> {
|
||||
let user = get_user(&user.id).await?;
|
||||
|
||||
validate_api_key_name(&self.name)?;
|
||||
|
||||
let key = format!("K-{}", random_string(SECRET_LENGTH));
|
||||
let secret = format!("S-{}", random_string(SECRET_LENGTH));
|
||||
let secret_hash = bcrypt::hash(&secret, BCRYPT_COST)
|
||||
.context("failed at hashing secret string")?;
|
||||
.context("Failed at hashing secret string")?;
|
||||
|
||||
let api_key = ApiKey {
|
||||
name: self.name,
|
||||
@@ -170,11 +180,13 @@ impl Resolve<UserArgs> for CreateApiKey {
|
||||
created_at: komodo_timestamp(),
|
||||
expires: self.expires,
|
||||
};
|
||||
|
||||
db_client()
|
||||
.api_keys
|
||||
.insert_one(api_key)
|
||||
.await
|
||||
.context("failed to create api key on db")?;
|
||||
.context("Failed to create api key on database")?;
|
||||
|
||||
Ok(CreateApiKeyResponse { key, secret })
|
||||
}
|
||||
}
|
||||
@@ -190,20 +202,27 @@ impl Resolve<UserArgs> for DeleteApiKey {
|
||||
UserArgs { user }: &UserArgs,
|
||||
) -> serror::Result<DeleteApiKeyResponse> {
|
||||
let client = db_client();
|
||||
|
||||
let key = client
|
||||
.api_keys
|
||||
.find_one(doc! { "key": &self.key })
|
||||
.await
|
||||
.context("failed at db query")?
|
||||
.context("no api key with key found")?;
|
||||
.context("Failed at database query")?
|
||||
.context("No api key with key found")?;
|
||||
|
||||
if user.id != key.user_id {
|
||||
return Err(anyhow!("api key does not belong to user").into());
|
||||
return Err(
|
||||
anyhow!("Api key does not belong to user")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
client
|
||||
.api_keys
|
||||
.delete_one(doc! { "key": key.key })
|
||||
.await
|
||||
.context("failed to delete api key from db")?;
|
||||
.context("Failed to delete api key from database")?;
|
||||
|
||||
Ok(DeleteApiKeyResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{
|
||||
by_id::find_one_by_id,
|
||||
mongodb::bson::{doc, oid::ObjectId},
|
||||
};
|
||||
use database::mungos::{by_id::find_one_by_id, mongodb::bson::doc};
|
||||
use komodo_client::{
|
||||
api::{user::CreateApiKey, write::*},
|
||||
entities::{
|
||||
@@ -12,9 +7,15 @@ use komodo_client::{
|
||||
user::{User, UserConfig},
|
||||
},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::{api::user::UserArgs, state::db_client};
|
||||
use crate::{
|
||||
api::user::UserArgs,
|
||||
helpers::validations::{validate_api_key_name, validate_username},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
@@ -33,16 +34,18 @@ impl Resolve<WriteArgs> for CreateServiceUser {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CreateServiceUserResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin").into());
|
||||
}
|
||||
if ObjectId::from_str(&self.username).is_ok() {
|
||||
return Err(
|
||||
anyhow!("username cannot be valid ObjectId").into(),
|
||||
anyhow!("Only Admins can manage Service Users")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
validate_username(&self.username)?;
|
||||
|
||||
let config = UserConfig::Service {
|
||||
description: self.description,
|
||||
};
|
||||
|
||||
let mut user = User {
|
||||
id: Default::default(),
|
||||
username: self.username,
|
||||
@@ -57,6 +60,7 @@ impl Resolve<WriteArgs> for CreateServiceUser {
|
||||
all: Default::default(),
|
||||
updated_at: komodo_timestamp(),
|
||||
};
|
||||
|
||||
user.id = db_client()
|
||||
.users
|
||||
.insert_one(&user)
|
||||
@@ -66,6 +70,7 @@ impl Resolve<WriteArgs> for CreateServiceUser {
|
||||
.as_object_id()
|
||||
.context("inserted id is not object id")?
|
||||
.to_string();
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
@@ -85,18 +90,28 @@ impl Resolve<WriteArgs> for UpdateServiceUserDescription {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateServiceUserDescriptionResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin").into());
|
||||
return Err(
|
||||
anyhow!("Only Admins can manage Service Users")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let db = db_client();
|
||||
|
||||
let service_user = db
|
||||
.users
|
||||
.find_one(doc! { "username": &self.username })
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("no user with given username")?;
|
||||
.context("Failed to query db for user")?
|
||||
.context("No user with given username")?;
|
||||
|
||||
let UserConfig::Service { .. } = &service_user.config else {
|
||||
return Err(anyhow!("user is not service user").into());
|
||||
return Err(
|
||||
anyhow!("Target user is not Service User")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
};
|
||||
|
||||
db.users
|
||||
.update_one(
|
||||
doc! { "username": &self.username },
|
||||
@@ -104,13 +119,15 @@ impl Resolve<WriteArgs> for UpdateServiceUserDescription {
|
||||
)
|
||||
.await
|
||||
.context("failed to update user on db")?;
|
||||
let res = db
|
||||
|
||||
let service_user = db
|
||||
.users
|
||||
.find_one(doc! { "username": &self.username })
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("user with username not found")?;
|
||||
Ok(res)
|
||||
|
||||
Ok(service_user)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,16 +147,27 @@ impl Resolve<WriteArgs> for CreateApiKeyForServiceUser {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CreateApiKeyForServiceUserResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin").into());
|
||||
return Err(
|
||||
anyhow!("Only Admins can manage Service Users")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
validate_api_key_name(&self.name)?;
|
||||
|
||||
let service_user =
|
||||
find_one_by_id(&db_client().users, &self.user_id)
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("no user found with id")?;
|
||||
.context("Failed to query db for user")?
|
||||
.context("No user found with id")?;
|
||||
|
||||
let UserConfig::Service { .. } = &service_user.config else {
|
||||
return Err(anyhow!("user is not service user").into());
|
||||
return Err(
|
||||
anyhow!("Target user is not Service User")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
};
|
||||
|
||||
CreateApiKey {
|
||||
name: self.name,
|
||||
expires: self.expires,
|
||||
@@ -163,23 +191,34 @@ impl Resolve<WriteArgs> for DeleteApiKeyForServiceUser {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<DeleteApiKeyForServiceUserResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin").into());
|
||||
return Err(
|
||||
anyhow!("Only Admins can manage Service Users")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let db = db_client();
|
||||
|
||||
let api_key = db
|
||||
.api_keys
|
||||
.find_one(doc! { "key": &self.key })
|
||||
.await
|
||||
.context("failed to query db for api key")?
|
||||
.context("did not find matching api key")?;
|
||||
|
||||
let service_user =
|
||||
find_one_by_id(&db_client().users, &api_key.user_id)
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("no user found with id")?;
|
||||
|
||||
let UserConfig::Service { .. } = &service_user.config else {
|
||||
return Err(anyhow!("user is not service user").into());
|
||||
return Err(
|
||||
anyhow!("Target user is not Service User")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
};
|
||||
|
||||
db.api_keys
|
||||
.delete_one(doc! { "key": self.key })
|
||||
.await
|
||||
|
||||
@@ -17,7 +17,11 @@ use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::{config::core_config, state::db_client};
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::validations::{validate_password, validate_username},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
@@ -38,24 +42,13 @@ impl Resolve<WriteArgs> for CreateLocalUser {
|
||||
) -> serror::Result<CreateLocalUserResponse> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin-only.")
|
||||
anyhow!("This method is Admin Only.")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
if self.username.is_empty() {
|
||||
return Err(anyhow!("Username cannot be empty.").into());
|
||||
}
|
||||
|
||||
if ObjectId::from_str(&self.username).is_ok() {
|
||||
return Err(
|
||||
anyhow!("Username cannot be valid ObjectId").into(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.password.is_empty() {
|
||||
return Err(anyhow!("Password cannot be empty.").into());
|
||||
}
|
||||
validate_username(&self.username)?;
|
||||
validate_password(&self.password)?;
|
||||
|
||||
let db = db_client();
|
||||
|
||||
@@ -130,17 +123,11 @@ impl Resolve<WriteArgs> for UpdateUserUsername {
|
||||
);
|
||||
}
|
||||
}
|
||||
if self.username.is_empty() {
|
||||
return Err(anyhow!("Username cannot be empty.").into());
|
||||
}
|
||||
|
||||
if ObjectId::from_str(&self.username).is_ok() {
|
||||
return Err(
|
||||
anyhow!("Username cannot be valid ObjectId").into(),
|
||||
);
|
||||
}
|
||||
validate_username(&self.username)?;
|
||||
|
||||
let db = db_client();
|
||||
|
||||
if db
|
||||
.users
|
||||
.find_one(doc! { "username": &self.username })
|
||||
@@ -150,8 +137,10 @@ impl Resolve<WriteArgs> for UpdateUserUsername {
|
||||
{
|
||||
return Err(anyhow!("Username already taken.").into());
|
||||
}
|
||||
|
||||
let id = ObjectId::from_str(&user.id)
|
||||
.context("User id not valid ObjectId.")?;
|
||||
|
||||
db.users
|
||||
.update_one(
|
||||
doc! { "_id": id },
|
||||
@@ -159,6 +148,7 @@ impl Resolve<WriteArgs> for UpdateUserUsername {
|
||||
)
|
||||
.await
|
||||
.context("Failed to update user username on database.")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
@@ -185,7 +175,11 @@ impl Resolve<WriteArgs> for UpdateUserPassword {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validate_password(&self.password)?;
|
||||
|
||||
db_client().set_user_password(user, &self.password).await?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
@@ -211,15 +205,19 @@ impl Resolve<WriteArgs> for DeleteUser {
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
if admin.username == self.user || admin.id == self.user {
|
||||
return Err(anyhow!("User cannot delete themselves.").into());
|
||||
}
|
||||
|
||||
let query = if let Ok(id) = ObjectId::from_str(&self.user) {
|
||||
doc! { "_id": id }
|
||||
} else {
|
||||
doc! { "username": self.user }
|
||||
};
|
||||
|
||||
let db = db_client();
|
||||
|
||||
let Some(user) = db
|
||||
.users
|
||||
.find_one(query.clone())
|
||||
@@ -230,21 +228,25 @@ impl Resolve<WriteArgs> for DeleteUser {
|
||||
anyhow!("No user found with given id / username").into(),
|
||||
);
|
||||
};
|
||||
|
||||
if user.super_admin {
|
||||
return Err(
|
||||
anyhow!("Cannot delete a super admin user.").into(),
|
||||
);
|
||||
}
|
||||
|
||||
if user.admin && !admin.super_admin {
|
||||
return Err(
|
||||
anyhow!("Only a Super Admin can delete an admin user.")
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
db.users
|
||||
.delete_one(query)
|
||||
.await
|
||||
.context("Failed to delete user from database")?;
|
||||
|
||||
// Also remove user id from all user groups
|
||||
if let Err(e) = db
|
||||
.user_groups
|
||||
@@ -253,6 +255,7 @@ impl Resolve<WriteArgs> for DeleteUser {
|
||||
{
|
||||
warn!("Failed to remove deleted user from user groups | {e:?}");
|
||||
};
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::{
|
||||
helpers::{
|
||||
query::get_variable,
|
||||
update::{add_update, make_update},
|
||||
validations::{validate_variable_name, validate_variable_value},
|
||||
},
|
||||
state::db_client,
|
||||
};
|
||||
@@ -35,7 +36,7 @@ impl Resolve<WriteArgs> for CreateVariable {
|
||||
) -> serror::Result<CreateVariableResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can create variables")
|
||||
anyhow!("Only Admins can create Variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
@@ -47,6 +48,9 @@ impl Resolve<WriteArgs> for CreateVariable {
|
||||
is_secret,
|
||||
} = self;
|
||||
|
||||
validate_variable_name(&name)?;
|
||||
validate_variable_value(&value)?;
|
||||
|
||||
let variable = Variable {
|
||||
name,
|
||||
value,
|
||||
@@ -58,7 +62,7 @@ impl Resolve<WriteArgs> for CreateVariable {
|
||||
.variables
|
||||
.insert_one(&variable)
|
||||
.await
|
||||
.context("Failed to create variable on db")?;
|
||||
.context("Failed to create Variable on db")?;
|
||||
|
||||
let mut update = make_update(
|
||||
ResourceTarget::system(),
|
||||
@@ -67,7 +71,8 @@ impl Resolve<WriteArgs> for CreateVariable {
|
||||
);
|
||||
|
||||
update
|
||||
.push_simple_log("create variable", format!("{variable:#?}"));
|
||||
.push_simple_log("Create Variable", format!("{variable:#?}"));
|
||||
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
@@ -91,13 +96,16 @@ impl Resolve<WriteArgs> for UpdateVariableValue {
|
||||
) -> serror::Result<UpdateVariableValueResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can update variables")
|
||||
anyhow!("Only Admins can update Variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let UpdateVariableValue { name, value } = self;
|
||||
|
||||
validate_variable_name(&name)?;
|
||||
validate_variable_value(&value)?;
|
||||
|
||||
let variable = get_variable(&name).await?;
|
||||
|
||||
if value == variable.value {
|
||||
@@ -156,10 +164,11 @@ impl Resolve<WriteArgs> for UpdateVariableDescription {
|
||||
) -> serror::Result<UpdateVariableDescriptionResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can update variables")
|
||||
anyhow!("Only Admins can update Variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
db_client()
|
||||
.variables
|
||||
.update_one(
|
||||
@@ -168,6 +177,7 @@ impl Resolve<WriteArgs> for UpdateVariableDescription {
|
||||
)
|
||||
.await
|
||||
.context("Failed to update variable description on db")?;
|
||||
|
||||
Ok(get_variable(&self.name).await?)
|
||||
}
|
||||
}
|
||||
@@ -188,10 +198,11 @@ impl Resolve<WriteArgs> for UpdateVariableIsSecret {
|
||||
) -> serror::Result<UpdateVariableIsSecretResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can update variables")
|
||||
anyhow!("Only Admins can update Variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
db_client()
|
||||
.variables
|
||||
.update_one(
|
||||
@@ -199,7 +210,8 @@ impl Resolve<WriteArgs> for UpdateVariableIsSecret {
|
||||
doc! { "$set": { "is_secret": self.is_secret } },
|
||||
)
|
||||
.await
|
||||
.context("Failed to update variable is secret on db")?;
|
||||
.context("Failed to update Variable 'is_secret' on db")?;
|
||||
|
||||
Ok(get_variable(&self.name).await?)
|
||||
}
|
||||
}
|
||||
@@ -219,16 +231,18 @@ impl Resolve<WriteArgs> for DeleteVariable {
|
||||
) -> serror::Result<DeleteVariableResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can delete variables")
|
||||
anyhow!("Only Admins can delete Variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let variable = get_variable(&self.name).await?;
|
||||
|
||||
db_client()
|
||||
.variables
|
||||
.delete_one(doc! { "name": &self.name })
|
||||
.await
|
||||
.context("Failed to delete variable on db")?;
|
||||
.context("Failed to delete Variable on db")?;
|
||||
|
||||
let mut update = make_update(
|
||||
ResourceTarget::system(),
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use database::{
|
||||
hash_password,
|
||||
mungos::mongodb::bson::{Document, doc, oid::ObjectId},
|
||||
mungos::mongodb::bson::{Document, doc},
|
||||
};
|
||||
use komodo_client::{
|
||||
api::auth::{
|
||||
@@ -18,6 +16,7 @@ use resolver_api::Resolve;
|
||||
use crate::{
|
||||
api::auth::AuthArgs,
|
||||
config::core_config,
|
||||
helpers::validations::{validate_password, validate_username},
|
||||
state::{db_client, jwt_client},
|
||||
};
|
||||
|
||||
@@ -33,19 +32,8 @@ impl Resolve<AuthArgs> for SignUpLocalUser {
|
||||
return Err(anyhow!("Local auth is not enabled").into());
|
||||
}
|
||||
|
||||
if self.username.is_empty() {
|
||||
return Err(anyhow!("Username cannot be empty string").into());
|
||||
}
|
||||
|
||||
if ObjectId::from_str(&self.username).is_ok() {
|
||||
return Err(
|
||||
anyhow!("Username cannot be valid ObjectId").into(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.password.is_empty() {
|
||||
return Err(anyhow!("Password cannot be empty string").into());
|
||||
}
|
||||
validate_username(&self.username)?;
|
||||
validate_password(&self.password)?;
|
||||
|
||||
let db = db_client();
|
||||
|
||||
@@ -90,15 +78,15 @@ impl Resolve<AuthArgs> for SignUpLocalUser {
|
||||
.users
|
||||
.insert_one(user)
|
||||
.await
|
||||
.context("failed to create user")?
|
||||
.context("Failed to create user on database")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.context("The 'inserted_id' is not ObjectId")?
|
||||
.to_string();
|
||||
|
||||
jwt_client()
|
||||
.encode(user_id.clone())
|
||||
.context("failed to generate jwt for user")
|
||||
.encode(user_id)
|
||||
.context("Failed to generate JWT for user")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -112,6 +100,8 @@ impl Resolve<AuthArgs> for LoginLocalUser {
|
||||
return Err(anyhow!("local auth is not enabled").into());
|
||||
}
|
||||
|
||||
validate_username(&self.username)?;
|
||||
|
||||
let user = db_client()
|
||||
.users
|
||||
.find_one(doc! { "username": &self.username })
|
||||
@@ -127,7 +117,7 @@ impl Resolve<AuthArgs> for LoginLocalUser {
|
||||
else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"non-local auth users can not log in with a password"
|
||||
"Non-local auth users can not log in with a password"
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
@@ -141,8 +131,8 @@ impl Resolve<AuthArgs> for LoginLocalUser {
|
||||
}
|
||||
|
||||
jwt_client()
|
||||
.encode(user.id.clone())
|
||||
.context("failed at generating jwt for user")
|
||||
.encode(user.id)
|
||||
.context("Failed to generate JWT for user")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +374,9 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
.komodo_lock_login_credentials_for
|
||||
.unwrap_or(config.lock_login_credentials_for),
|
||||
local_auth: env.komodo_local_auth.unwrap_or(config.local_auth),
|
||||
min_password_length: env
|
||||
.komodo_min_password_length
|
||||
.unwrap_or(config.min_password_length),
|
||||
logging: LogConfig {
|
||||
level: env
|
||||
.komodo_logging_level
|
||||
|
||||
@@ -33,8 +33,7 @@ pub mod query;
|
||||
pub mod swarm;
|
||||
pub mod terminal;
|
||||
pub mod update;
|
||||
|
||||
// pub mod resource;
|
||||
pub mod validations;
|
||||
|
||||
pub fn empty_or_only_spaces(word: &str) -> bool {
|
||||
if word.is_empty() {
|
||||
|
||||
85
bin/core/src/helpers/validations.rs
Normal file
85
bin/core/src/helpers/validations.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
//! # Input Validation Module
|
||||
//!
|
||||
//! This module provides validation functions for user inputs to prevent
|
||||
//! invalid data from entering the system and improve security.
|
||||
|
||||
use anyhow::Context;
|
||||
use validations::{StringValidator, StringValidatorMatches};
|
||||
|
||||
use crate::config::core_config;
|
||||
|
||||
/// Minimum length for usernames
|
||||
pub const MIN_USERNAME_LENGTH: usize = 1;
|
||||
/// Maximum length for usernames
|
||||
pub const MAX_USERNAME_LENGTH: usize = 100;
|
||||
|
||||
/// Validate usernames
|
||||
///
|
||||
/// - Between [MIN_USERNAME_LENGTH] and [MAX_USERNAME_LENGTH] characters
|
||||
/// - Matches `^[a-zA-Z0-9._@-]+$`
|
||||
pub fn validate_username(username: &str) -> anyhow::Result<()> {
|
||||
StringValidator::default()
|
||||
.min_length(MIN_USERNAME_LENGTH)
|
||||
.max_length(MAX_USERNAME_LENGTH)
|
||||
.matches(StringValidatorMatches::Username)
|
||||
.validate(username)
|
||||
.context("Failed to validate username")
|
||||
}
|
||||
|
||||
/// Maximum length for passwords
|
||||
pub const MAX_PASSWORD_LENGTH: usize = 1000;
|
||||
|
||||
/// Validate passwords
|
||||
///
|
||||
/// - Between [CoreConfig::min_password_length][komodo_client::entities::config::core::CoreConfig::min_password_length] and [MAX_PASSWORD_LENGTH] characters
|
||||
pub fn validate_password(password: &str) -> anyhow::Result<()> {
|
||||
StringValidator::default()
|
||||
.min_length(core_config().min_password_length as usize)
|
||||
.max_length(MAX_PASSWORD_LENGTH)
|
||||
.validate(password)
|
||||
.context("Failed to validate password")
|
||||
}
|
||||
|
||||
/// Maximum length for API key names
|
||||
pub const MAX_API_KEY_NAME_LENGTH: usize = 200;
|
||||
|
||||
/// Validate api key names
|
||||
///
|
||||
/// - Greater than [MAX_API_KEY_NAME_LENGTH] characters
|
||||
pub fn validate_api_key_name(name: &str) -> anyhow::Result<()> {
|
||||
StringValidator::default()
|
||||
.max_length(MAX_API_KEY_NAME_LENGTH)
|
||||
.validate(name)
|
||||
.context("Failed to validate api key name")
|
||||
}
|
||||
|
||||
/// Minimum length for variable names
|
||||
pub const MIN_VARIABLE_NAME_LENGTH: usize = 1;
|
||||
/// Maximum length for variable names
|
||||
pub const MAX_VARIABLE_NAME_LENGTH: usize = 500;
|
||||
|
||||
/// Validate variable names
|
||||
///
|
||||
/// - Between [MIN_VARIABLE_NAME_LENGTH] and [MAX_VARIABLE_NAME_LENGTH] characters
|
||||
/// - Matches `^[a-zA-Z_][a-zA-Z0-9_]*$`
|
||||
pub fn validate_variable_name(name: &str) -> anyhow::Result<()> {
|
||||
StringValidator::default()
|
||||
.min_length(MIN_VARIABLE_NAME_LENGTH)
|
||||
.max_length(MAX_VARIABLE_NAME_LENGTH)
|
||||
.matches(StringValidatorMatches::VariableName)
|
||||
.validate(name)
|
||||
.context("Failed to validate variable name")
|
||||
}
|
||||
|
||||
/// Maximum length for variable values
|
||||
pub const MAX_VARIABLE_VALUE_LENGTH: usize = 10000;
|
||||
|
||||
/// Validate variable values
|
||||
///
|
||||
/// - Less than [MAX_VARIABLE_VALUE_LENGTH] characters
|
||||
pub fn validate_variable_value(value: &str) -> anyhow::Result<()> {
|
||||
StringValidator::default()
|
||||
.max_length(MAX_VARIABLE_VALUE_LENGTH)
|
||||
.validate(value)
|
||||
.context("Failed to validate variable value")
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod github;
|
||||
pub mod gitlab;
|
||||
@@ -6,9 +6,7 @@ extern crate tracing;
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{Router, routing::get};
|
||||
use axum_server::{Handle, tls_rustls::RustlsConfig};
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::config::{core_config, core_keys};
|
||||
@@ -20,7 +18,6 @@ mod cloud;
|
||||
mod config;
|
||||
mod connection;
|
||||
mod helpers;
|
||||
mod listener;
|
||||
mod monitor;
|
||||
mod network;
|
||||
mod periphery;
|
||||
@@ -32,7 +29,6 @@ mod startup;
|
||||
mod state;
|
||||
mod sync;
|
||||
mod ts_client;
|
||||
mod ws;
|
||||
|
||||
async fn app() -> anyhow::Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
@@ -86,27 +82,7 @@ async fn app() -> anyhow::Result<()> {
|
||||
.instrument(startup_span)
|
||||
.await;
|
||||
|
||||
// Setup static frontend services
|
||||
let frontend_path = &config.frontend_path;
|
||||
let frontend_index =
|
||||
ServeFile::new(format!("{frontend_path}/index.html"));
|
||||
let serve_frontend = ServeDir::new(frontend_path)
|
||||
.not_found_service(frontend_index.clone());
|
||||
|
||||
let app = Router::new()
|
||||
.route("/version", get(|| async { env!("CARGO_PKG_VERSION") }))
|
||||
.nest("/auth", api::auth::router())
|
||||
.nest("/user", api::user::router())
|
||||
.nest("/read", api::read::router())
|
||||
.nest("/write", api::write::router())
|
||||
.nest("/execute", api::execute::router())
|
||||
.nest("/terminal", api::terminal::router())
|
||||
.nest("/listener", listener::router())
|
||||
.nest("/ws", ws::router())
|
||||
.nest("/client", ts_client::router())
|
||||
.fallback_service(serve_frontend)
|
||||
.layer(config::cors_layer())
|
||||
.into_make_service();
|
||||
let app = api::app().into_make_service();
|
||||
|
||||
let addr =
|
||||
format!("{}:{}", core_config().bind_ip, core_config().port);
|
||||
|
||||
@@ -166,6 +166,8 @@ pub struct Env {
|
||||
|
||||
/// Override `local_auth`
|
||||
pub komodo_local_auth: Option<bool>,
|
||||
/// Override `min_password_length`
|
||||
pub komodo_min_password_length: Option<u16>,
|
||||
/// Override `init_admin_username`
|
||||
pub komodo_init_admin_username: Option<String>,
|
||||
/// Override `init_admin_username` from file
|
||||
@@ -410,14 +412,20 @@ pub struct CoreConfig {
|
||||
// ================
|
||||
// = Auth / Login =
|
||||
// ================
|
||||
/// enable login with local auth
|
||||
/// Enable login with local auth
|
||||
#[serde(default)]
|
||||
pub local_auth: bool,
|
||||
|
||||
/// Configure a minimum password length.
|
||||
/// Default: 1
|
||||
#[serde(default = "default_min_password_length")]
|
||||
pub min_password_length: u16,
|
||||
|
||||
/// Upon fresh launch, initalize an Admin user with this username.
|
||||
/// If this is not provided, no initial user will be created.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub init_admin_username: Option<String>,
|
||||
|
||||
/// Upon fresh launch, initalize an Admin user with this password.
|
||||
/// Default: `changeme`
|
||||
#[serde(default = "default_init_admin_password")]
|
||||
@@ -701,6 +709,10 @@ fn default_jwt_ttl() -> Timelength {
|
||||
Timelength::OneDay
|
||||
}
|
||||
|
||||
fn default_min_password_length() -> u16 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_init_admin_password() -> String {
|
||||
String::from("changeme")
|
||||
}
|
||||
@@ -759,6 +771,7 @@ impl Default for CoreConfig {
|
||||
frontend_path: default_frontend_path(),
|
||||
database: Default::default(),
|
||||
local_auth: Default::default(),
|
||||
min_password_length: default_min_password_length(),
|
||||
init_admin_username: Default::default(),
|
||||
init_admin_password: default_init_admin_password(),
|
||||
transparent_mode: Default::default(),
|
||||
@@ -846,6 +859,7 @@ impl CoreConfig {
|
||||
disable_non_admin_create: config.disable_non_admin_create,
|
||||
lock_login_credentials_for: config.lock_login_credentials_for,
|
||||
local_auth: config.local_auth,
|
||||
min_password_length: config.min_password_length,
|
||||
init_admin_username: config
|
||||
.init_admin_username
|
||||
.map(|u| empty_or_redacted(&u)),
|
||||
|
||||
14
lib/validations/Cargo.toml
Normal file
14
lib/validations/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "validations"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
regex.workspace = true
|
||||
bson.workspace = true
|
||||
url.workspace = true
|
||||
175
lib/validations/src/lib.rs
Normal file
175
lib/validations/src/lib.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
//! # Input Validation Module
|
||||
//!
|
||||
//! This module provides validation functions for user inputs to prevent
|
||||
//! invalid data from entering the system and improve security.
|
||||
|
||||
use std::{str::FromStr as _, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use bson::oid::ObjectId;
|
||||
use regex::Regex;
|
||||
|
||||
/// Options to validate input strings to have certain properties.
|
||||
/// This ensures only valid data can enter the system.
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// ```
|
||||
/// StringValidator::default()
|
||||
/// .min_length(1)
|
||||
/// .max_length(100)
|
||||
/// .matches(StringValidatorMatches::Username)
|
||||
/// .validate("admin@example.com")?
|
||||
/// ```
|
||||
#[derive(Default)]
|
||||
pub struct StringValidator {
|
||||
/// Specify the minimum length of string.
|
||||
/// Setting `0` will effectively skip this validation.
|
||||
pub min_length: usize,
|
||||
/// Specify max length of string, or None to allow arbitrary length.
|
||||
pub max_length: Option<usize>,
|
||||
/// Skip the control character check.
|
||||
/// Most values should not contain these by default.
|
||||
pub skip_control_check: bool,
|
||||
/// Specify a pattern to validate the string contents.
|
||||
pub matches: Option<StringValidatorMatches>,
|
||||
}
|
||||
|
||||
impl StringValidator {
|
||||
/// Returns Ok if input passes validations, otherwise includes
|
||||
/// error with failure reason.
|
||||
pub fn validate(&self, input: &str) -> anyhow::Result<()> {
|
||||
let len = input.len();
|
||||
|
||||
if len < self.min_length {
|
||||
return Err(anyhow!(
|
||||
"Input too short. Must be at least {} characters.",
|
||||
self.min_length
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(max_length) = self.max_length
|
||||
&& len > max_length
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Input too long. Must be at most {max_length} characters."
|
||||
));
|
||||
}
|
||||
|
||||
if !self.skip_control_check {
|
||||
validate_no_control_chars(input)?;
|
||||
}
|
||||
|
||||
if let Some(matches) = &self.matches {
|
||||
matches.validate(input)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn min_length(mut self, min_length: usize) -> StringValidator {
|
||||
self.min_length = min_length;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_length(
|
||||
mut self,
|
||||
max_length: impl Into<Option<usize>>,
|
||||
) -> StringValidator {
|
||||
self.max_length = max_length.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn skip_control_check(mut self) -> StringValidator {
|
||||
self.skip_control_check = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn matches(
|
||||
mut self,
|
||||
matches: impl Into<Option<StringValidatorMatches>>,
|
||||
) -> StringValidator {
|
||||
self.matches = matches.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub enum StringValidatorMatches {
|
||||
/// - alphanumeric characters
|
||||
/// - underscores
|
||||
/// - hyphens
|
||||
/// - dots
|
||||
/// - @
|
||||
/// - No Object Ids
|
||||
Username,
|
||||
/// - alphanumeric characters
|
||||
/// - underscores
|
||||
VariableName,
|
||||
/// - http or https URL.
|
||||
HttpUrl,
|
||||
}
|
||||
|
||||
impl StringValidatorMatches {
|
||||
/// Returns Ok if input passes validations, otherwise includes
|
||||
/// error with failure reason.
|
||||
fn validate(&self, input: &str) -> anyhow::Result<()> {
|
||||
let validate = || match self {
|
||||
StringValidatorMatches::Username => {
|
||||
static USERNAME_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
let regex = USERNAME_REGEX.get_or_init(|| {
|
||||
Regex::new(r"^[a-zA-Z0-9._@-]+$")
|
||||
.expect("Failed to initialize username regex")
|
||||
});
|
||||
if !regex.is_match(input) {
|
||||
return Err(anyhow!(
|
||||
"Only alphanumeric characters, underscores, hyphens, dots, and @ are allowed"
|
||||
));
|
||||
}
|
||||
if ObjectId::from_str(input).is_ok() {
|
||||
return Err(anyhow!("Cannot be valid ObjectId").into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
StringValidatorMatches::VariableName => {
|
||||
static VARIABLE_NAME_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
let regex = VARIABLE_NAME_REGEX.get_or_init(|| {
|
||||
Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
||||
.expect("Failed to initialize variable name regex")
|
||||
});
|
||||
if regex.is_match(input) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Only alphanumeric characters and underscores are allowed"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
StringValidatorMatches::HttpUrl => {
|
||||
if !input.starts_with("http://")
|
||||
&& !input.starts_with("https://")
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Input must start with http:// or https://"
|
||||
));
|
||||
}
|
||||
url::Url::parse(input)
|
||||
.context("Failed to parse input as URL")
|
||||
.map(|_| ())
|
||||
}
|
||||
};
|
||||
validate().context("Invalid characters in input")
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_no_control_chars(input: &str) -> anyhow::Result<()> {
|
||||
for (index, char) in input.chars().enumerate() {
|
||||
if char.is_control() {
|
||||
return Err(anyhow!(
|
||||
"Control character at index {index}. Input: \"{input}\""
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user